From ae903f8e718d89cdff5538bf2d5c93ec7c59c360 Mon Sep 17 00:00:00 2001 From: Tavis Aitken Date: Tue, 27 Apr 2021 11:01:56 -0600 Subject: [PATCH 001/206] Added config schema to the Splunk On Call plugin. Signed-off-by: Tavis Aitken --- .changeset/fresh-vans-nail.md | 5 +++++ plugins/splunk-on-call/package.json | 6 ++++-- plugins/splunk-on-call/schema.d.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 .changeset/fresh-vans-nail.md create mode 100644 plugins/splunk-on-call/schema.d.ts diff --git a/.changeset/fresh-vans-nail.md b/.changeset/fresh-vans-nail.md new file mode 100644 index 0000000000..64af4ee60b --- /dev/null +++ b/.changeset/fresh-vans-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-splunk-on-call': patch +--- + +Added config schema to expose `splunkOnCall.eventsRestEndpoint` config option to the frontend diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 9e93004bf5..b033b41e75 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -59,6 +59,8 @@ "node-fetch": "^2.6.1" }, "files": [ - "dist" - ] + "dist", + "schema.d.ts" + ], + "configSchema": "schema.d.ts" } diff --git a/plugins/splunk-on-call/schema.d.ts b/plugins/splunk-on-call/schema.d.ts new file mode 100644 index 0000000000..b14f2ef110 --- /dev/null +++ b/plugins/splunk-on-call/schema.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Config { + /** + * Splunk On Call Plugin specific configs + */ + splunkOnCall: { + /** + * @visibility frontend + */ + eventsRestEndpoint: string; + }; +} From d2329e7486ee0507921290289399a979cce03d05 Mon Sep 17 00:00:00 2001 From: Tejas Kumar Date: Thu, 20 May 2021 16:23:13 +0200 Subject: [PATCH 002/206] Use stringifyEntityRef in place of deprecated Signed-off-by: Tejas Kumar --- plugins/techdocs-backend/src/DocsBuilder/builder.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index d1aa1e08be..4c4744879c 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -16,7 +16,7 @@ import { Entity, ENTITY_DEFAULT_NAMESPACE, - serializeEntityRef, + stringifyEntityRef, } from '@backstage/catalog-model'; import { NotModifiedError } from '@backstage/errors'; import { @@ -75,7 +75,7 @@ export class DocsBuilder { */ this.logger.info( - `Step 1 of 3: Preparing docs for entity ${serializeEntityRef( + `Step 1 of 3: Preparing docs for entity ${stringifyEntityRef( this.entity, )}`, ); @@ -116,7 +116,7 @@ export class DocsBuilder { // Set last check happened to now new BuildMetadataStorage(this.entity.metadata.uid).setLastUpdated(); this.logger.debug( - `Docs for ${serializeEntityRef( + `Docs for ${stringifyEntityRef( this.entity, )} are unmodified. Using cache, skipping generate and prepare`, ); @@ -126,7 +126,7 @@ export class DocsBuilder { } this.logger.info( - `Prepare step completed for entity ${serializeEntityRef( + `Prepare step completed for entity ${stringifyEntityRef( this.entity, )}, stored at ${preparedDir}`, ); @@ -136,7 +136,7 @@ export class DocsBuilder { */ this.logger.info( - `Step 2 of 3: Generating docs for entity ${serializeEntityRef( + `Step 2 of 3: Generating docs for entity ${stringifyEntityRef( this.entity, )}`, ); @@ -176,7 +176,7 @@ export class DocsBuilder { */ this.logger.info( - `Step 3 of 3: Publishing docs for entity ${serializeEntityRef( + `Step 3 of 3: Publishing docs for entity ${stringifyEntityRef( this.entity, )}`, ); From a345a2e019e0f33210cd72772c4990624644e093 Mon Sep 17 00:00:00 2001 From: Tejas Kumar Date: Thu, 20 May 2021 16:32:31 +0200 Subject: [PATCH 003/206] Use backend.workingDir for techdocs Signed-off-by: Tejas Kumar --- plugins/techdocs-backend/src/DocsBuilder/builder.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 4c4744879c..8495e88d37 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { getRootLogger, loadBackendConfig } from '@backstage/backend-common'; import { Entity, ENTITY_DEFAULT_NAMESPACE, @@ -142,7 +143,12 @@ export class DocsBuilder { ); // Create a temporary directory to store the generated files in. - const tmpdirPath = os.tmpdir(); + const config = await loadBackendConfig({ + argv: process.argv, + logger: getRootLogger(), + }); + const workingDir = config.get('backend.workingDirectory'); + const tmpdirPath = workingDir ? String(workingDir) : os.tmpdir(); // Fixes a problem with macOS returning a path that is a symlink const tmpdirResolvedPath = fs.realpathSync(tmpdirPath); const outputDir = await fs.mkdtemp( From d93a3f98533e496fc5a1f045f4deb49b5e8e7c53 Mon Sep 17 00:00:00 2001 From: Tejas Kumar Date: Thu, 20 May 2021 16:34:04 +0200 Subject: [PATCH 004/206] Refactor to use optional string Signed-off-by: Tejas Kumar --- plugins/techdocs-backend/src/DocsBuilder/builder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 8495e88d37..e43ed72c31 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -147,8 +147,8 @@ export class DocsBuilder { argv: process.argv, logger: getRootLogger(), }); - const workingDir = config.get('backend.workingDirectory'); - const tmpdirPath = workingDir ? String(workingDir) : os.tmpdir(); + const workingDir = config.getOptionalString('backend.workingDirectory'); + const tmpdirPath = workingDir || os.tmpdir(); // Fixes a problem with macOS returning a path that is a symlink const tmpdirResolvedPath = fs.realpathSync(tmpdirPath); const outputDir = await fs.mkdtemp( From 6013a16dc37ac5a0d2b61b650e05f993dca0cc31 Mon Sep 17 00:00:00 2001 From: Tejas Kumar Date: Thu, 20 May 2021 16:35:11 +0200 Subject: [PATCH 005/206] Add Changeset Signed-off-by: Tejas Kumar --- .changeset/nasty-wasps-look.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nasty-wasps-look.md diff --git a/.changeset/nasty-wasps-look.md b/.changeset/nasty-wasps-look.md new file mode 100644 index 0000000000..6de02cbb04 --- /dev/null +++ b/.changeset/nasty-wasps-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +TechDocs: Support configurable working directory as temp dir From 35cb3c21cf789e77bdbf20532ee83c1a5a3b9ab4 Mon Sep 17 00:00:00 2001 From: RISHABH BUDHIRAJA Date: Fri, 21 May 2021 01:36:28 +0530 Subject: [PATCH 006/206] FIx: Diagram component using hardcoded namespace Signed-off-by: RISHABH BUDHIRAJA --- .../src/components/SystemDiagramCard/SystemDiagramCard.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx index 5fd32a6298..42d493912c 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx @@ -149,6 +149,7 @@ export function SystemDiagramCard() { const currentSystemNode = stringifyEntityRef(entity); const systemNodes = new Array<{ id: string; kind: string; name: string }>(); const systemEdges = new Array<{ from: string; to: string; label: string }>(); + const ref = parseEntityRef(currentSystemNode); const catalogApi = useApi(catalogApiRef); const { loading, error, value: catalogResponse } = useAsync(() => { @@ -157,7 +158,7 @@ export function SystemDiagramCard() { kind: ['Component', 'API', 'Resource', 'System', 'Domain'], 'spec.system': [ currentSystemName, - `${ENTITY_DEFAULT_NAMESPACE}/${currentSystemName}`, + `${ref.namespace || 'Current Namespace'}/${currentSystemName}`, ], }, }); From 804a605ad84593411c69d9729c301a867987e0d0 Mon Sep 17 00:00:00 2001 From: RISHABH BUDHIRAJA Date: Wed, 26 May 2021 01:20:05 +0530 Subject: [PATCH 007/206] Revert "FIx: Diagram component using hardcoded namespace" This reverts commit dd3fc3e6616ab801e7fc1c9deeb7270f07cb3892. Signed-off-by: RISHABH BUDHIRAJA --- .../src/components/SystemDiagramCard/SystemDiagramCard.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx index 42d493912c..5fd32a6298 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx @@ -149,7 +149,6 @@ export function SystemDiagramCard() { const currentSystemNode = stringifyEntityRef(entity); const systemNodes = new Array<{ id: string; kind: string; name: string }>(); const systemEdges = new Array<{ from: string; to: string; label: string }>(); - const ref = parseEntityRef(currentSystemNode); const catalogApi = useApi(catalogApiRef); const { loading, error, value: catalogResponse } = useAsync(() => { @@ -158,7 +157,7 @@ export function SystemDiagramCard() { kind: ['Component', 'API', 'Resource', 'System', 'Domain'], 'spec.system': [ currentSystemName, - `${ref.namespace || 'Current Namespace'}/${currentSystemName}`, + `${ENTITY_DEFAULT_NAMESPACE}/${currentSystemName}`, ], }, }); From 6a584214360aaff67670bde6ccfb7d9738dd3792 Mon Sep 17 00:00:00 2001 From: RISHABH BUDHIRAJA Date: Wed, 26 May 2021 01:20:59 +0530 Subject: [PATCH 008/206] Fix: Diagram component using hardcoded namespace Signed-off-by: RISHABH BUDHIRAJA --- .../src/components/SystemDiagramCard/SystemDiagramCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx index 5fd32a6298..4e34fb1910 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx @@ -157,7 +157,7 @@ export function SystemDiagramCard() { kind: ['Component', 'API', 'Resource', 'System', 'Domain'], 'spec.system': [ currentSystemName, - `${ENTITY_DEFAULT_NAMESPACE}/${currentSystemName}`, + `${entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE}/${currentSystemName}`, ], }, }); From d2d42a7fa29ed9d83b750e5177b5606d725a943b Mon Sep 17 00:00:00 2001 From: RISHABH BUDHIRAJA Date: Wed, 26 May 2021 18:44:15 +0530 Subject: [PATCH 009/206] adds changeset for patch Signed-off-by: RISHABH BUDHIRAJA --- .changeset/thick-donkeys-fold.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thick-donkeys-fold.md diff --git a/.changeset/thick-donkeys-fold.md b/.changeset/thick-donkeys-fold.md new file mode 100644 index 0000000000..4720439479 --- /dev/null +++ b/.changeset/thick-donkeys-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fix for Diagram component using hardcoded namespace From 8650112701671551b34cc553abfaf6db24cf3cd9 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 3 Jun 2021 09:49:18 +0200 Subject: [PATCH 010/206] Catalog: Enable new processing engine Signed-off-by: Johan Haals --- packages/backend/src/plugins/catalog.ts | 44 ++-- .../20210302150147_refresh_state.js | 0 .../migrationsv2/20200511113813_init.js | 133 ---------- ...0200520140700_location_update_log_table.js | 43 ---- ...7114117_location_update_log_latest_view.js | 45 ---- .../migrationsv2/20200702153613_entities.js | 236 ------------------ ..._location_update_log_latest_deduplicate.js | 44 ---- ...904_location_update_log_duplication_fix.js | 87 ------- .../20200807120600_entitySearch.js | 41 --- .../20200809202832_add_bootstrap_location.js | 42 ---- .../20200923104503_case_insensitivity.js | 32 --- .../20201005122705_add_entity_full_name.js | 60 ----- .../20201006130744_entity_data_column.js | 66 ----- ...6203131_entity_remove_redundant_columns.js | 50 ---- .../20201007201501_index_entity_search.js | 37 --- .../20201019130742_add_relations_table.js | 54 ---- .../20201123205611_relations_table_uniq.js | 93 ------- .../migrationsv2/20201210185851_fk_index.js | 45 ---- .../20201230103504_update_log_varchar.js | 73 ------ .../20210209121210_locations_fk_index.js | 45 ---- .../src/next/NextCatalogBuilder.ts | 2 +- 21 files changed, 23 insertions(+), 1249 deletions(-) rename plugins/catalog-backend/{migrationsv2 => migrations}/20210302150147_refresh_state.js (100%) delete mode 100644 plugins/catalog-backend/migrationsv2/20200511113813_init.js delete mode 100644 plugins/catalog-backend/migrationsv2/20200520140700_location_update_log_table.js delete mode 100644 plugins/catalog-backend/migrationsv2/20200527114117_location_update_log_latest_view.js delete mode 100644 plugins/catalog-backend/migrationsv2/20200702153613_entities.js delete mode 100644 plugins/catalog-backend/migrationsv2/20200721115244_location_update_log_latest_deduplicate.js delete mode 100644 plugins/catalog-backend/migrationsv2/20200805163904_location_update_log_duplication_fix.js delete mode 100644 plugins/catalog-backend/migrationsv2/20200807120600_entitySearch.js delete mode 100644 plugins/catalog-backend/migrationsv2/20200809202832_add_bootstrap_location.js delete mode 100644 plugins/catalog-backend/migrationsv2/20200923104503_case_insensitivity.js delete mode 100644 plugins/catalog-backend/migrationsv2/20201005122705_add_entity_full_name.js delete mode 100644 plugins/catalog-backend/migrationsv2/20201006130744_entity_data_column.js delete mode 100644 plugins/catalog-backend/migrationsv2/20201006203131_entity_remove_redundant_columns.js delete mode 100644 plugins/catalog-backend/migrationsv2/20201007201501_index_entity_search.js delete mode 100644 plugins/catalog-backend/migrationsv2/20201019130742_add_relations_table.js delete mode 100644 plugins/catalog-backend/migrationsv2/20201123205611_relations_table_uniq.js delete mode 100644 plugins/catalog-backend/migrationsv2/20201210185851_fk_index.js delete mode 100644 plugins/catalog-backend/migrationsv2/20201230103504_update_log_varchar.js delete mode 100644 plugins/catalog-backend/migrationsv2/20210209121210_locations_fk_index.js diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 63a3e53c81..8474478f9e 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -29,49 +29,49 @@ export default async function createPlugin( env: PluginEnvironment, ): Promise { /* - * ** WARNING ** - * DO NOT enable the experimental catalog, it will brick your database migrations. - * This is solely for internal backstage development. + * This environment variable exists as an emergency option during the release + * of the new catalog processing engine. + * If you experience any issues, make sure to report them as this flag + * will be removed in a subsequent release. */ - if (process.env.EXPERIMENTAL_CATALOG === '1') { - const builder = new NextCatalogBuilder(env); + if (process.env.LEGACY_CATALOG === '1') { + const builder = new CatalogBuilder(env); const { entitiesCatalog, + locationsCatalog, + higherOrderOperation, locationAnalyzer, - processingEngine, - locationService, } = await builder.build(); - // TODO(jhaals): run and manage in background. - await processingEngine.start(); + useHotCleanup( + module, + runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000), + ); - return await createNextRouter({ + return await createRouter({ entitiesCatalog, + locationsCatalog, + higherOrderOperation, locationAnalyzer, - locationService, logger: env.logger, config: env.config, }); } - - const builder = new CatalogBuilder(env); + const builder = new NextCatalogBuilder(env); const { entitiesCatalog, - locationsCatalog, - higherOrderOperation, locationAnalyzer, + processingEngine, + locationService, } = await builder.build(); - useHotCleanup( - module, - runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000), - ); + // TODO(jhaals): run and manage in background. + await processingEngine.start(); - return await createRouter({ + return await createNextRouter({ entitiesCatalog, - locationsCatalog, - higherOrderOperation, locationAnalyzer, + locationService, logger: env.logger, config: env.config, }); diff --git a/plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js b/plugins/catalog-backend/migrations/20210302150147_refresh_state.js similarity index 100% rename from plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js rename to plugins/catalog-backend/migrations/20210302150147_refresh_state.js diff --git a/plugins/catalog-backend/migrationsv2/20200511113813_init.js b/plugins/catalog-backend/migrationsv2/20200511113813_init.js deleted file mode 100644 index 7f3d75e35c..0000000000 --- a/plugins/catalog-backend/migrationsv2/20200511113813_init.js +++ /dev/null @@ -1,133 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - return ( - knex.schema - // - // locations - // - .createTable('locations', table => { - table.comment( - 'Registered locations that shall be contiuously scanned for catalog item updates', - ); - table - .uuid('id') - .primary() - .notNullable() - .comment('Auto-generated ID of the location'); - table.string('type').notNullable().comment('The type of location'); - table - .string('target') - .notNullable() - .comment('The actual target of the location'); - }) - // - // entities - // - .createTable('entities', table => { - table.comment('All entities currently stored in the catalog'); - table.uuid('id').primary().comment('Auto-generated ID of the entity'); - table - .uuid('location_id') - .references('id') - .inTable('locations') - .nullable() - .comment('The location that originated the entity'); - table - .string('etag') - .notNullable() - .comment( - 'An opaque string that changes for each update operation to any part of the entity, including metadata.', - ); - table - .string('generation') - .notNullable() - .unsigned() - .comment( - 'A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes.', - ); - table - .string('api_version') - .notNullable() - .comment('The apiVersion field of the entity'); - table - .string('kind') - .notNullable() - .comment('The kind field of the entity'); - table - .string('name') - .nullable() - .comment('The metadata.name field of the entity'); - table - .string('namespace') - .nullable() - .comment('The metadata.namespace field of the entity'); - table - .string('metadata') - .notNullable() - .comment('The entire metadata JSON blob of the entity'); - table - .string('spec') - .nullable() - .comment('The entire spec JSON blob of the entity'); - }) - .alterTable('entities', table => { - // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - table.unique(['kind', 'name', 'namespace'], 'entities_unique_name'); - }) - // - // entities_search - // - .createTable('entities_search', table => { - table.comment( - 'Flattened key-values from the entities, used for quick filtering', - ); - table - .uuid('entity_id') - .references('id') - .inTable('entities') - .onDelete('CASCADE') - .comment('The entity that matches this key/value'); - table - .string('key') - .notNullable() - .comment('A key that occurs in the entity'); - table - .string('value') - .nullable() - .comment('The corresponding value to match on'); - }) - ); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - return knex.schema - .dropTable('entities_search') - .alterTable('entities', table => { - table.dropUnique([], 'entities_unique_name'); - }) - .dropTable('entities') - .dropTable('locations'); -}; diff --git a/plugins/catalog-backend/migrationsv2/20200520140700_location_update_log_table.js b/plugins/catalog-backend/migrationsv2/20200520140700_location_update_log_table.js deleted file mode 100644 index d8093fc9b4..0000000000 --- a/plugins/catalog-backend/migrationsv2/20200520140700_location_update_log_table.js +++ /dev/null @@ -1,43 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - return knex.schema.createTable('location_update_log', table => { - table.uuid('id').primary(); - table.enum('status', ['success', 'fail']).notNullable(); - table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable(); - table.string('message'); - table - .uuid('location_id') - .references('id') - .inTable('locations') - .onUpdate('CASCADE') - .onDelete('CASCADE'); - table.string('entity_name').nullable(); - }); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - return knex.schema.dropTableIfExists('location_update_log'); -}; diff --git a/plugins/catalog-backend/migrationsv2/20200527114117_location_update_log_latest_view.js b/plugins/catalog-backend/migrationsv2/20200527114117_location_update_log_latest_view.js deleted file mode 100644 index a0f0f33a65..0000000000 --- a/plugins/catalog-backend/migrationsv2/20200527114117_location_update_log_latest_view.js +++ /dev/null @@ -1,45 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - // Get list sorted by created_at timestamp in descending order - // Grouped by location_id - return knex.schema.raw(` - CREATE VIEW location_update_log_latest AS - SELECT t1.* FROM location_update_log t1 - JOIN - ( - SELECT location_id, MAX(created_at) AS MAXDATE - FROM location_update_log - GROUP BY location_id - ) t2 - ON t1.location_id = t2.location_id - AND t1.created_at = t2.MAXDATE - ORDER BY created_at DESC; - `); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - return knex.schema.raw(`DROP VIEW location_update_log_latest;`); -}; diff --git a/plugins/catalog-backend/migrationsv2/20200702153613_entities.js b/plugins/catalog-backend/migrationsv2/20200702153613_entities.js deleted file mode 100644 index 0f1c204f9b..0000000000 --- a/plugins/catalog-backend/migrationsv2/20200702153613_entities.js +++ /dev/null @@ -1,236 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - // SQLite does not support FK and PK - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities_search', table => { - table.dropForeign(['entity_id']); - }); - await knex.schema.alterTable('entities', table => { - table.dropPrimary('entities_pkey'); - }); - } - await knex.schema.alterTable('entities', table => { - table.dropUnique([], 'entities_unique_name'); - }); - // Setup temporary tables - await knex.schema.renameTable('entities_search', 'tmp_entities_search'); - await knex.schema.renameTable('entities', 'tmp_entities'); - - // - // entities - // - await knex.schema - .createTable('entities', table => { - table.comment('All entities currently stored in the catalog'); - table.uuid('id').primary().comment('Auto-generated ID of the entity'); - table - .uuid('location_id') - .references('id') - .inTable('locations') - .nullable() - .comment('The location that originated the entity'); - table - .string('etag') - .notNullable() - .comment( - 'An opaque string that changes for each update operation to any part of the entity, including metadata.', - ); - table - .string('generation') - .notNullable() - .unsigned() - .comment( - 'A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes.', - ); - table - .string('api_version') - .notNullable() - .comment('The apiVersion field of the entity'); - table - .string('kind') - .notNullable() - .comment('The kind field of the entity'); - table - .string('name') - .nullable() - .comment('The metadata.name field of the entity'); - table - .string('namespace') - .nullable() - .comment('The metadata.namespace field of the entity'); - table - .text('metadata') - .notNullable() - .comment('The entire metadata JSON blob of the entity'); - table - .text('spec') - .nullable() - .comment('The entire spec JSON blob of the entity'); - }) - .alterTable('entities', table => { - // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - table.unique(['kind', 'name', 'namespace'], 'entities_unique_name'); - }); - - await knex.schema.raw(`INSERT INTO entities SELECT * FROM tmp_entities`); - - // - // entities_search - // - await knex.schema.createTable('entities_search', table => { - table.comment( - 'Flattened key-values from the entities, used for quick filtering', - ); - table - .uuid('entity_id') - .references('id') - .inTable('entities') - .onDelete('CASCADE') - .comment('The entity that matches this key/value'); - table - .string('key') - .notNullable() - .comment('A key that occurs in the entity'); - table - .string('value') - .nullable() - .comment('The corresponding value to match on'); - }); - await knex.schema.raw( - `INSERT INTO entities_search SELECT * FROM tmp_entities_search`, - ); - - // Clean up - await knex.schema.dropTable('tmp_entities'); - return knex.schema.dropTable('tmp_entities_search'); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - // SQLite does not support FK and PK - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities_search', table => { - table.dropForeign(['entity_id']); - }); - await knex.schema.alterTable('entities', table => { - table.dropPrimary('entities_pkey'); - }); - } - await knex.schema.alterTable('entities', table => { - table.dropUnique([], 'entities_unique_name'); - }); - - // Setup temporary tables - await knex.schema.renameTable('entities_search', 'tmp_entities_search'); - await knex.schema.renameTable('entities', 'tmp_entities'); - - // - // entities - // - await knex.schema - .createTable('entities', table => { - table.comment('All entities currently stored in the catalog'); - table.uuid('id').primary().comment('Auto-generated ID of the entity'); - table - .uuid('location_id') - .references('id') - .inTable('locations') - .nullable() - .comment('The location that originated the entity'); - table - .string('etag') - .notNullable() - .comment( - 'An opaque string that changes for each update operation to any part of the entity, including metadata.', - ); - table - .string('generation') - .notNullable() - .unsigned() - .comment( - 'A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes.', - ); - table - .string('api_version') - .notNullable() - .comment('The apiVersion field of the entity'); - table - .string('kind') - .notNullable() - .comment('The kind field of the entity'); - table - .string('name') - .nullable() - .comment('The metadata.name field of the entity'); - table - .string('namespace') - .nullable() - .comment('The metadata.namespace field of the entity'); - table - .string('metadata') - .notNullable() - .comment('The entire metadata JSON blob of the entity'); - table - .string('spec') - .nullable() - .comment('The entire spec JSON blob of the entity'); - }) - .alterTable('entities', table => { - // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - table.unique(['kind', 'name', 'namespace'], 'entities_unique_name'); - }); - - await knex.schema.raw(`INSERT INTO entities SELECT * FROM tmp_entities`); - - // - // entities_search - // - await knex.schema.createTable('entities_search', table => { - table.comment( - 'Flattened key-values from the entities, used for quick filtering', - ); - table - .uuid('entity_id') - .references('id') - .inTable('entities') - .onDelete('CASCADE') - .comment('The entity that matches this key/value'); - table - .string('key') - .notNullable() - .comment('A key that occurs in the entity'); - table - .string('value') - .nullable() - .comment('The corresponding value to match on'); - }); - await knex.schema.raw( - `INSERT INTO entities_search SELECT * FROM tmp_entities_search`, - ); - - // Clean up - await knex.schema.dropTable('tmp_entities'); - return knex.schema.dropTable('tmp_entities_search'); -}; diff --git a/plugins/catalog-backend/migrationsv2/20200721115244_location_update_log_latest_deduplicate.js b/plugins/catalog-backend/migrationsv2/20200721115244_location_update_log_latest_deduplicate.js deleted file mode 100644 index 87b41a80fc..0000000000 --- a/plugins/catalog-backend/migrationsv2/20200721115244_location_update_log_latest_deduplicate.js +++ /dev/null @@ -1,44 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = function up(knex) { - return knex.schema.raw(`DROP VIEW location_update_log_latest;`).raw(` - CREATE VIEW location_update_log_latest AS - SELECT t1.* FROM location_update_log t1 - JOIN - ( - SELECT location_id, MAX(created_at) AS MAXDATE - FROM location_update_log - GROUP BY location_id - ) t2 - ON t1.location_id = t2.location_id - AND t1.created_at = t2.MAXDATE - GROUP BY t1.location_id, t1.id - ORDER BY created_at DESC; -`); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = function down(knex) { - knex.schema.raw(`DROP VIEW location_update_log_latest;`); -}; diff --git a/plugins/catalog-backend/migrationsv2/20200805163904_location_update_log_duplication_fix.js b/plugins/catalog-backend/migrationsv2/20200805163904_location_update_log_duplication_fix.js deleted file mode 100644 index de2b194cff..0000000000 --- a/plugins/catalog-backend/migrationsv2/20200805163904_location_update_log_duplication_fix.js +++ /dev/null @@ -1,87 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = function up(knex) { - return knex.schema - .raw('DROP VIEW location_update_log_latest;') - .dropTable('location_update_log') - .createTable('location_update_log', table => { - table.bigIncrements('id').primary(); // instead of uuid, so we can MAX it - table.enum('status', ['success', 'fail']).notNullable(); - table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable(); - table.string('message'); - table - .uuid('location_id') - .references('id') - .inTable('locations') - .onUpdate('CASCADE') - .onDelete('CASCADE'); - table.string('entity_name').nullable(); - }).raw(` - CREATE VIEW location_update_log_latest AS - SELECT t1.* FROM location_update_log t1 - JOIN - ( - SELECT location_id, MAX(id) AS MAXID - FROM location_update_log - GROUP BY location_id - ) t2 - ON t1.location_id = t2.location_id - AND t1.id = t2.MAXID - GROUP BY t1.location_id, t1.id - ORDER BY created_at DESC; - `); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = function down(knex) { - return knex.schema - .raw('DROP VIEW location_update_log_latest;') - .dropTable('location_update_log') - .createTable('location_update_log', table => { - table.uuid('id').primary(); - table.enum('status', ['success', 'fail']).notNullable(); - table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable(); - table.string('message'); - table - .uuid('location_id') - .references('id') - .inTable('locations') - .onUpdate('CASCADE') - .onDelete('CASCADE'); - table.string('entity_name').nullable(); - }).raw(` - CREATE VIEW location_update_log_latest AS - SELECT t1.* FROM location_update_log t1 - JOIN - ( - SELECT location_id, MAX(created_at) AS MAXDATE - FROM location_update_log - GROUP BY location_id - ) t2 - ON t1.location_id = t2.location_id - AND t1.created_at = t2.MAXDATE - GROUP BY t1.location_id, t1.id - ORDER BY created_at DESC; - `); -}; diff --git a/plugins/catalog-backend/migrationsv2/20200807120600_entitySearch.js b/plugins/catalog-backend/migrationsv2/20200807120600_entitySearch.js deleted file mode 100644 index 45226e53b4..0000000000 --- a/plugins/catalog-backend/migrationsv2/20200807120600_entitySearch.js +++ /dev/null @@ -1,41 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - // Sqlite does not support alter column. - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities_search', table => { - table.text('value').nullable().alter(); - }); - } -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - // Sqlite does not support alter column. - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities_search', table => { - table.string('value').nullable().alter(); - }); - } -}; diff --git a/plugins/catalog-backend/migrationsv2/20200809202832_add_bootstrap_location.js b/plugins/catalog-backend/migrationsv2/20200809202832_add_bootstrap_location.js deleted file mode 100644 index a90813fe85..0000000000 --- a/plugins/catalog-backend/migrationsv2/20200809202832_add_bootstrap_location.js +++ /dev/null @@ -1,42 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - // Adds a single 'bootstrap' location that can be used to trigger work in processors. - // This is primarily here to fulfill foreign key constraints. - await knex('locations').insert({ - id: require('uuid').v4(), - type: 'bootstrap', - target: 'bootstrap', - }); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - await knex('locations') - .where({ - type: 'bootstrap', - target: 'bootstrap', - }) - .del(); -}; diff --git a/plugins/catalog-backend/migrationsv2/20200923104503_case_insensitivity.js b/plugins/catalog-backend/migrationsv2/20200923104503_case_insensitivity.js deleted file mode 100644 index ea5ba9e58d..0000000000 --- a/plugins/catalog-backend/migrationsv2/20200923104503_case_insensitivity.js +++ /dev/null @@ -1,32 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - await knex('entities') - .where({ namespace: null }) - .update({ namespace: 'default' }); - await knex('entities_search').update({ - key: knex.raw('LOWER(key)'), - value: knex.raw('LOWER(value)'), - }); -}; - -exports.down = async function down() {}; diff --git a/plugins/catalog-backend/migrationsv2/20201005122705_add_entity_full_name.js b/plugins/catalog-backend/migrationsv2/20201005122705_add_entity_full_name.js deleted file mode 100644 index aae1861658..0000000000 --- a/plugins/catalog-backend/migrationsv2/20201005122705_add_entity_full_name.js +++ /dev/null @@ -1,60 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - await knex.schema.alterTable('entities', table => { - table.text('full_name').nullable(); - }); - - await knex('entities').update({ - full_name: knex.raw( - "LOWER(kind) || ':' || LOWER(COALESCE(namespace, 'default')) || '/' || LOWER(name)", - ), - }); - - // SQLite does not support alter column - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities', table => { - table.text('full_name').notNullable().alter(); - }); - } - - await knex.schema.alterTable('entities', table => { - // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - table.unique(['full_name'], 'entities_unique_full_name'); - table.dropUnique([], 'entities_unique_name'); - }); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - await knex.schema.alterTable('entities', table => { - // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - table.dropUnique([], 'entities_unique_full_name'); - table.unique(['kind', 'namespace', 'name'], 'entities_unique_name'); - }); - - await knex.schema.alterTable('entities_search', table => { - table.dropColumn('full_name'); - }); -}; diff --git a/plugins/catalog-backend/migrationsv2/20201006130744_entity_data_column.js b/plugins/catalog-backend/migrationsv2/20201006130744_entity_data_column.js deleted file mode 100644 index a8964efbf6..0000000000 --- a/plugins/catalog-backend/migrationsv2/20201006130744_entity_data_column.js +++ /dev/null @@ -1,66 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - await knex.schema.alterTable('entities', table => { - table - .text('data') - .nullable() - .comment('The entire JSON data blob of the entity'); - }); - - await knex('entities').update({ - // apiVersion and kind should not contain any JSON unsafe chars, and both - // metadata and spec are already valid serialized JSON - data: knex.raw( - `'{"apiVersion":"' || api_version || '","kind":"' || kind || '","metadata":' || metadata || COALESCE(',"spec":' || spec, '') || '}'`, - ), - }); - - await knex.schema.alterTable('entities', table => { - table.dropColumn('metadata'); - table.dropColumn('spec'); - }); - - // SQLite does not support ALTER COLUMN. - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities', table => { - table.text('data').notNullable().alter(); - }); - } -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - await knex.schema.alterTable('entities', table => { - table - .text('metadata') - .notNullable() - .comment('The entire metadata JSON blob of the entity'); - table - .text('spec') - .nullable() - .comment('The entire spec JSON blob of the entity'); - table.dropColumn('data'); - }); -}; diff --git a/plugins/catalog-backend/migrationsv2/20201006203131_entity_remove_redundant_columns.js b/plugins/catalog-backend/migrationsv2/20201006203131_entity_remove_redundant_columns.js deleted file mode 100644 index f40df5f73e..0000000000 --- a/plugins/catalog-backend/migrationsv2/20201006203131_entity_remove_redundant_columns.js +++ /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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - await knex.schema.alterTable('entities', table => { - table.dropColumn('api_version'); - table.dropColumn('kind'); - table.dropColumn('name'); - table.dropColumn('namespace'); - }); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - await knex.schema.alterTable('entities', table => { - table - .string('api_version') - .notNullable() - .comment('The apiVersion field of the entity'); - table.string('kind').notNullable().comment('The kind field of the entity'); - table - .string('name') - .nullable() - .comment('The metadata.name field of the entity'); - table - .string('namespace') - .nullable() - .comment('The metadata.namespace field of the entity'); - }); -}; diff --git a/plugins/catalog-backend/migrationsv2/20201007201501_index_entity_search.js b/plugins/catalog-backend/migrationsv2/20201007201501_index_entity_search.js deleted file mode 100644 index 77bf0529eb..0000000000 --- a/plugins/catalog-backend/migrationsv2/20201007201501_index_entity_search.js +++ /dev/null @@ -1,37 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - await knex.schema.alterTable('entities_search', table => { - table.index(['key'], 'entities_search_key'); - table.index(['value'], 'entities_search_value'); - }); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - await knex.schema.alterTable('entities_search', table => { - table.dropIndex('', 'entities_search_key'); - table.dropIndex('', 'entities_search_value'); - }); -}; diff --git a/plugins/catalog-backend/migrationsv2/20201019130742_add_relations_table.js b/plugins/catalog-backend/migrationsv2/20201019130742_add_relations_table.js deleted file mode 100644 index 85e729f814..0000000000 --- a/plugins/catalog-backend/migrationsv2/20201019130742_add_relations_table.js +++ /dev/null @@ -1,54 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - await knex.schema.createTable('entities_relations', table => { - table.comment('All relations between entities in the catalog'); - table - .uuid('originating_entity_id') - .references('id') - .inTable('entities') - .onDelete('CASCADE') - .notNullable() - .comment('The entity that provided the relation'); - table - .string('source_full_name') - .notNullable() - .comment('The full name of the source entity of the relation'); - table - .string('type') - .notNullable() - .comment('The type of the relation between the entities'); - table - .string('target_full_name') - .notNullable() - .comment('The full name of the target entity of the relation'); - - table.primary(['source_full_name', 'type', 'target_full_name']); - }); -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - await knex.schema.dropTable('entities_relations'); -}; diff --git a/plugins/catalog-backend/migrationsv2/20201123205611_relations_table_uniq.js b/plugins/catalog-backend/migrationsv2/20201123205611_relations_table_uniq.js deleted file mode 100644 index 9e8198b5eb..0000000000 --- a/plugins/catalog-backend/migrationsv2/20201123205611_relations_table_uniq.js +++ /dev/null @@ -1,93 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - if (knex.client.config.client === 'sqlite3') { - // sqlite doesn't support dropPrimary so we recreate it properly instead - await knex.schema.dropTable('entities_relations'); - await knex.schema.createTable('entities_relations', table => { - table.comment('All relations between entities in the catalog'); - table - .uuid('originating_entity_id') - .references('id') - .inTable('entities') - .onDelete('CASCADE') - .notNullable() - .comment('The entity that provided the relation'); - table - .string('source_full_name') - .notNullable() - .comment('The full name of the source entity of the relation'); - table - .string('type') - .notNullable() - .comment('The type of the relation between the entities'); - table - .string('target_full_name') - .notNullable() - .comment('The full name of the target entity of the relation'); - table.index('source_full_name', 'source_full_name_idx'); - }); - } else { - await knex.schema.alterTable('entities_relations', table => { - table.dropPrimary(); - table.index('source_full_name', 'source_full_name_idx'); - }); - } -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - if (knex.client.config.client === 'sqlite3') { - await knex.schema.dropTable('entities_relations'); - await knex.schema.createTable('entities_relations', table => { - table.comment('All relations between entities in the catalog'); - table - .uuid('originating_entity_id') - .references('id') - .inTable('entities') - .onDelete('CASCADE') - .notNullable() - .comment('The entity that provided the relation'); - table - .string('source_full_name') - .notNullable() - .comment('The full name of the source entity of the relation'); - table - .string('type') - .notNullable() - .comment('The type of the relation between the entities'); - table - .string('target_full_name') - .notNullable() - .comment('The full name of the target entity of the relation'); - - table.primary(['source_full_name', 'type', 'target_full_name']); - }); - } else { - await knex.schema.alterTable('entities_relations', table => { - table.dropIndex([], 'source_full_name_idx'); - table.primary(['source_full_name', 'type', 'target_full_name']); - }); - } -}; diff --git a/plugins/catalog-backend/migrationsv2/20201210185851_fk_index.js b/plugins/catalog-backend/migrationsv2/20201210185851_fk_index.js deleted file mode 100644 index abb26cd5fc..0000000000 --- a/plugins/catalog-backend/migrationsv2/20201210185851_fk_index.js +++ /dev/null @@ -1,45 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities_relations', table => { - table.index('originating_entity_id', 'originating_entity_id_idx'); - }); - await knex.schema.alterTable('entities_search', table => { - table.index('entity_id', 'entity_id_idx'); - }); - } -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities_relations', table => { - table.dropIndex([], 'originating_entity_id_idx'); - }); - await knex.schema.alterTable('entities_relations', table => { - table.dropIndex([], 'entity_id_idx'); - }); - } -}; diff --git a/plugins/catalog-backend/migrationsv2/20201230103504_update_log_varchar.js b/plugins/catalog-backend/migrationsv2/20201230103504_update_log_varchar.js deleted file mode 100644 index d924b0414a..0000000000 --- a/plugins/catalog-backend/migrationsv2/20201230103504_update_log_varchar.js +++ /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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - if (knex.client.config.client !== 'sqlite3') { - // We actually just want to widen columns, but can't do that while a - // view is dependent on them - so we just reconstruct it exactly as it was - await knex.schema - .raw('DROP VIEW location_update_log_latest;') - .alterTable('location_update_log', table => { - table.text('message').alter(); - table.text('entity_name').nullable().alter(); - }).raw(` - CREATE VIEW location_update_log_latest AS - SELECT t1.* FROM location_update_log t1 - JOIN - ( - SELECT location_id, MAX(id) AS MAXID - FROM location_update_log - GROUP BY location_id - ) t2 - ON t1.location_id = t2.location_id - AND t1.id = t2.MAXID - GROUP BY t1.location_id, t1.id - ORDER BY created_at DESC; - `); - } -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - if (knex.client.config.client !== 'sqlite3') { - await knex.schema - .raw('DROP VIEW location_update_log_latest;') - .alterTable('location_update_log', table => { - table.string('message').alter(); - table.string('entity_name').nullable().alter(); - }).raw(` - CREATE VIEW location_update_log_latest AS - SELECT t1.* FROM location_update_log t1 - JOIN - ( - SELECT location_id, MAX(id) AS MAXID - FROM location_update_log - GROUP BY location_id - ) t2 - ON t1.location_id = t2.location_id - AND t1.id = t2.MAXID - GROUP BY t1.location_id, t1.id - ORDER BY created_at DESC; - `); - } -}; diff --git a/plugins/catalog-backend/migrationsv2/20210209121210_locations_fk_index.js b/plugins/catalog-backend/migrationsv2/20210209121210_locations_fk_index.js deleted file mode 100644 index ccfb1faffb..0000000000 --- a/plugins/catalog-backend/migrationsv2/20210209121210_locations_fk_index.js +++ /dev/null @@ -1,45 +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. - */ - -// @ts-check - -/** - * @param {import('knex').Knex} knex - */ -exports.up = async function up(knex) { - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities', table => { - table.index('location_id', 'entity_location_id_idx'); - }); - await knex.schema.alterTable('location_update_log', table => { - table.index('location_id', 'update_log_location_id_idx'); - }); - } -}; - -/** - * @param {import('knex').Knex} knex - */ -exports.down = async function down(knex) { - if (knex.client.config.client !== 'sqlite3') { - await knex.schema.alterTable('entities', table => { - table.dropIndex([], 'entity_location_id_idx'); - }); - await knex.schema.alterTable('location_update_log', table => { - table.dropIndex([], 'update_log_location_id_idx'); - }); - } -}; diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index e10d13c129..b6b0a04d04 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -246,7 +246,7 @@ export class NextCatalogBuilder { await dbClient.migrate.latest({ directory: resolvePackagePath( '@backstage/plugin-catalog-backend', - 'migrationsv2', + 'migrations', ), }); From b45e29410a8ec199840b3f36362c30e0a126e584 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 3 Jun 2021 13:19:57 +0200 Subject: [PATCH 011/206] Add changeset Signed-off-by: Johan Haals --- .changeset/dull-poets-learn.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/dull-poets-learn.md diff --git a/.changeset/dull-poets-learn.md b/.changeset/dull-poets-learn.md new file mode 100644 index 0000000000..0574d0ebc6 --- /dev/null +++ b/.changeset/dull-poets-learn.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +This release enables the new catalog processing engine which is a major milestone for the catalog! + +This update makes processing more scalable across multiple instances, adds support for deletions and ui flagging of entities that are no longer referenced by a location. + +As this is a major internal change we have taken some precaution by offering a `LEGACY_CATALOG=1` environment variable that you can set when starting the backend in order to run the previous version. If you do so for any reason make sure to raise an issue immediately as this is a temporary "break the glass" option which will be removed in a subsequent release. From f8e773e3e979564dd81ae3a476c8bf517cc2f8a6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Jun 2021 08:48:54 +0200 Subject: [PATCH 012/206] chore: Update migrations dir in tests Signed-off-by: Johan Haals --- plugins/catalog-backend/src/next/database/DatabaseManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/next/database/DatabaseManager.ts b/plugins/catalog-backend/src/next/database/DatabaseManager.ts index 22ae4e782f..d501bc6c61 100644 --- a/plugins/catalog-backend/src/next/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/next/database/DatabaseManager.ts @@ -36,7 +36,7 @@ export class DatabaseManager { ): Promise { const migrationsDir = resolvePackagePath( '@backstage/plugin-catalog-backend', - 'migrationsv2', + 'migrations', ); await knex.migrate.latest({ From 3db3b67f5f1b1b023bf7e3678de7b1efdba441c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jun 2021 04:08:47 +0000 Subject: [PATCH 013/206] chore(deps): bump http-proxy-middleware from 0.19.2 to 2.0.0 Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 0.19.2 to 2.0.0. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/master/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v0.19.2...v2.0.0) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- plugins/proxy-backend/package.json | 2 +- yarn.lock | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index d2b0d64678..e83b0d9710 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -33,7 +33,7 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "http-proxy-middleware": "^0.19.1", + "http-proxy-middleware": "^2.0.0", "morgan": "^1.10.0", "uuid": "^8.0.0", "winston": "^3.2.1", diff --git a/yarn.lock b/yarn.lock index 683ec0f557..f8ec474142 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6191,10 +6191,10 @@ "@types/http-proxy" "*" "@types/node" "*" -"@types/http-proxy@*", "@types/http-proxy@^1.17.4": - version "1.17.5" - resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz#c203c5e6e9dc6820d27a40eb1e511c70a220423d" - integrity sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q== +"@types/http-proxy@*", "@types/http-proxy@^1.17.4", "@types/http-proxy@^1.17.5": + version "1.17.6" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.6.tgz#62dc3fade227d6ac2862c8f19ee0da9da9fd8616" + integrity sha512-+qsjqR75S/ib0ig0R9WN+CDoZeOBU6F2XLewgC4KVgdXiNHiKKHFEMRHOrs5PbYE97D5vataw5wPj4KLYfUkuQ== dependencies: "@types/node" "*" @@ -15181,15 +15181,16 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy-middleware@^0.19.1: - version "0.19.2" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.2.tgz#ee73dcc8348165afefe8de2ff717751d181608ee" - integrity sha512-aYk1rTKqLTus23X3L96LGNCGNgWpG4cG0XoZIT1GUPhhulEHX/QalnO6Vbo+WmKWi4AL2IidjuC0wZtbpg0yhQ== +http-proxy-middleware@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.0.tgz#20d1ac3409199c83e5d0383ba6436b04e7acb9fe" + integrity sha512-S+RN5njuyvYV760aiVKnyuTXqUMcSIvYOsHA891DOVQyrdZOwaXtBHpt9FUVPEDAsOvsPArZp6VXQLs44yvkow== dependencies: + "@types/http-proxy" "^1.17.5" http-proxy "^1.18.1" - is-glob "^4.0.0" - lodash "^4.17.11" - micromatch "^3.1.10" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" http-proxy@^1.17.0, http-proxy@^1.18.1: version "1.18.1" @@ -16065,6 +16066,11 @@ is-plain-obj@^2.0.0: resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" From 090dfe65db7ba2f1a76cd009ab911cdfdae017be Mon Sep 17 00:00:00 2001 From: Carlo Colombo Date: Tue, 8 Jun 2021 14:47:21 +0200 Subject: [PATCH 014/206] Add supprt to enable LFS for hosted Bitbucket Signed-off-by: Carlo Colombo --- .changeset/lazy-cougars-rule.md | 5 ++ .../actions/builtin/publish/bitbucket.test.ts | 82 +++++++++++++++++++ .../actions/builtin/publish/bitbucket.ts | 43 +++++++++- 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 .changeset/lazy-cougars-rule.md diff --git a/.changeset/lazy-cougars-rule.md b/.changeset/lazy-cougars-rule.md new file mode 100644 index 0000000000..cc6ec91bd5 --- /dev/null +++ b/.changeset/lazy-cougars-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Adds support to enable LFS for hosted Bitbucket diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 40e7f613f2..f670cc715a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -173,6 +173,88 @@ describe('publish:bitbucket', () => { }); }); + describe('LFS for hosted bitbucket', () => { + const repoCreationResponse = { + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }; + + it('should call the correct APIs to enable LFS if requested and the host is hosted bitbucket', async () => { + expect.assertions(1); + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos', + (_, res, ctx) => { + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(repoCreationResponse), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/owner/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + return res(ctx.status(204)); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo', + enableLFS: true, + }, + }); + }); + + it('should report an error if enabling LFS fails', async () => { + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos', + (_, res, ctx) => { + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(repoCreationResponse), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/owner/repos/repo/enabled', + (_, res, ctx) => { + return res(ctx.status(500)); + }, + ), + ); + + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo', + enableLFS: true, + }, + }), + ).rejects.toThrow(/Failed to enable LFS/); + }); + }); + it('should call initAndPush with the correct values', async () => { server.use( rest.post( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 243e515b19..d543f4cdaa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -156,6 +156,32 @@ const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => { ); }; +const performEnableLFS = async (opts: { + authorization: string; + host: string; + owner: string; + repo: string; +}) => { + const { authorization, host, owner, repo } = opts; + + const options: RequestInit = { + method: 'PUT', + headers: { + Authorization: authorization, + }, + }; + + const { ok, status, statusText } = await fetch( + `https://${host}/rest/git-lfs/admin/projects/${owner}/repos/${repo}/enabled`, + options, + ); + + if (!ok) + throw new Error( + `Failed to enable LFS in the repository, ${status}: ${statusText}`, + ); +}; + export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; }) { @@ -166,6 +192,7 @@ export function createPublishBitbucketAction(options: { description: string; repoVisibility: 'private' | 'public'; sourcePath?: string; + enableLFS: boolean; }>({ id: 'publish:bitbucket', description: @@ -193,6 +220,11 @@ export function createPublishBitbucketAction(options: { 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', type: 'string', }, + enableLFS: { + title: + 'Enable LFS for the repository. Only available for hosted Bitbucket.', + type: 'boolean', + }, }, }, output: { @@ -210,7 +242,12 @@ export function createPublishBitbucketAction(options: { }, }, async handler(ctx) { - const { repoUrl, description, repoVisibility = 'private' } = ctx.input; + const { + repoUrl, + description, + repoVisibility = 'private', + enableLFS = false, + } = ctx.input; const { owner, repo, host } = parseRepoUrl(repoUrl); @@ -252,6 +289,10 @@ export function createPublishBitbucketAction(options: { logger: ctx.logger, }); + if (enableLFS && host !== 'bitbucket.org') { + await performEnableLFS({ authorization, host, owner, repo }); + } + ctx.output('remoteUrl', remoteUrl); ctx.output('repoContentsUrl', repoContentsUrl); }, From 6ffcf9ed854b29fead9d8270eb3285ac3224821a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 8 Jun 2021 14:17:42 +0200 Subject: [PATCH 015/206] fixup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/violet-birds-lay.md | 5 +++ packages/cli/package.json | 2 +- .../proxy-backend/src/service/router.test.ts | 40 ++++++++----------- plugins/proxy-backend/src/service/router.ts | 18 +++++---- yarn.lock | 36 ++++++++--------- 5 files changed, 51 insertions(+), 50 deletions(-) create mode 100644 .changeset/violet-birds-lay.md diff --git a/.changeset/violet-birds-lay.md b/.changeset/violet-birds-lay.md new file mode 100644 index 0000000000..841603058f --- /dev/null +++ b/.changeset/violet-birds-lay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-proxy-backend': patch +--- + +Bump http-proxy-middleware from 0.19.2 to 2.0.0 diff --git a/packages/cli/package.json b/packages/cli/package.json index 3f075824dc..450aa02ee4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -139,7 +139,7 @@ "@types/rollup-plugin-postcss": "^2.0.0", "@types/tar": "^4.0.3", "@types/webpack": "^4.41.7", - "@types/webpack-dev-server": "3.11.0", + "@types/webpack-dev-server": "^3.11.0", "@types/yarnpkg__lockfile": "^1.1.4", "del": "^5.1.0", "mock-fs": "^4.13.0", diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index 57b87754e2..8b88e45cc5 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -14,25 +14,19 @@ * limitations under the License. */ -import { buildMiddleware, createRouter } from './router'; import { getVoidLogger, loadBackendConfig, SingleHostDiscovery, } from '@backstage/backend-common'; -import createProxyMiddleware, { - Config as ProxyMiddlewareConfig, - Proxy, -} from 'http-proxy-middleware'; +import { Request, Response } from 'express'; import * as http from 'http'; +import { createProxyMiddleware, Options } from 'http-proxy-middleware'; +import { buildMiddleware, createRouter } from './router'; -jest.mock('http-proxy-middleware', () => { - return jest.fn().mockImplementation( - (): Proxy => { - return () => undefined; - }, - ); -}); +jest.mock('http-proxy-middleware', () => ({ + createProxyMiddleware: jest.fn(() => () => undefined), +})); const mockCreateProxyMiddleware = createProxyMiddleware as jest.MockedFunction< typeof createProxyMiddleware @@ -66,7 +60,7 @@ describe('buildMiddleware', () => { const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [ (pathname: string, req: Partial) => boolean, - ProxyMiddlewareConfig, + Options, ]; expect(filter('', { method: 'GET', headers: {} })).toBe(true); expect(filter('', { method: 'POST', headers: {} })).toBe(true); @@ -86,7 +80,7 @@ describe('buildMiddleware', () => { const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [ (pathname: string, req: Partial) => boolean, - ProxyMiddlewareConfig, + Options, ]; expect(filter('', { method: 'GET', headers: {} })).toBe(true); expect(filter('', { method: 'POST', headers: {} })).toBe(true); @@ -106,7 +100,7 @@ describe('buildMiddleware', () => { const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [ (pathname: string, req: Partial) => boolean, - ProxyMiddlewareConfig, + Options, ]; expect(filter('', { method: 'GET', headers: {} })).toBe(true); expect(filter('', { method: 'POST', headers: {} })).toBe(true); @@ -129,7 +123,7 @@ describe('buildMiddleware', () => { const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [ (pathname: string, req: Partial) => boolean, - ProxyMiddlewareConfig, + Options, ]; expect(filter('', { method: 'GET', headers: {} })).toBe(true); expect(filter('', { method: 'POST', headers: {} })).toBe(false); @@ -254,8 +248,7 @@ describe('buildMiddleware', () => { expect(createProxyMiddleware).toHaveBeenCalledTimes(1); - const config = mockCreateProxyMiddleware.mock - .calls[0][1] as ProxyMiddlewareConfig; + const config = mockCreateProxyMiddleware.mock.calls[0][1] as Options; const testClientResponse = { headers: { @@ -275,8 +268,8 @@ describe('buildMiddleware', () => { config.onProxyRes!( testClientResponse as http.IncomingMessage, - {} as http.IncomingMessage, - {} as http.ServerResponse, + {} as Request, + {} as Response, ); expect(Object.keys(testClientResponse.headers!)).toEqual([ @@ -298,8 +291,7 @@ describe('buildMiddleware', () => { expect(createProxyMiddleware).toHaveBeenCalledTimes(1); - const config = mockCreateProxyMiddleware.mock - .calls[0][1] as ProxyMiddlewareConfig; + const config = mockCreateProxyMiddleware.mock.calls[0][1] as Options; const testClientResponse = { headers: { @@ -313,8 +305,8 @@ describe('buildMiddleware', () => { config.onProxyRes!( testClientResponse as http.IncomingMessage, - {} as http.IncomingMessage, - {} as http.ServerResponse, + {} as Request, + {} as Response, ); expect(Object.keys(testClientResponse.headers!)).toEqual(['set-cookie']); diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index c3b1d056db..8957d4e8ce 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -17,9 +17,10 @@ import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; -import createProxyMiddleware, { - Config as ProxyMiddlewareConfig, - Proxy, +import { + createProxyMiddleware, + Options, + RequestHandler, } from 'http-proxy-middleware'; import { Logger } from 'winston'; import http from 'http'; @@ -52,7 +53,7 @@ export interface RouterOptions { discovery: PluginEndpointDiscovery; } -export interface ProxyConfig extends ProxyMiddlewareConfig { +export interface ProxyConfig extends Options { allowedMethods?: string[]; allowedHeaders?: string[]; } @@ -64,14 +65,17 @@ export function buildMiddleware( logger: Logger, route: string, config: string | ProxyConfig, -): Proxy { +): RequestHandler { const fullConfig = typeof config === 'string' ? { target: config } : { ...config }; // Validate that target is a valid URL. + if (typeof fullConfig.target !== 'string') { + throw new Error(`Proxy target must be a string`); + } try { // eslint-disable-next-line no-new - new URL(fullConfig.target!); + new URL(fullConfig.target! as string); } catch { throw new Error( `Proxy target is not a valid URL: ${fullConfig.target ?? ''}`, @@ -131,7 +135,7 @@ export function buildMiddleware( // 2. Only permit the allowed HTTP methods if configured // // We are filtering the proxy request headers here rather than in - // `onProxyReq` becuase when global-agent is enabled then `onProxyReq` + // `onProxyReq` because when global-agent is enabled then `onProxyReq` // fires _after_ the agent has already sent the headers to the proxy // target, causing a ERR_HTTP_HEADERS_SENT crash const filter = (_pathname: string, req: http.IncomingMessage): boolean => { diff --git a/yarn.lock b/yarn.lock index f8ec474142..3437ae6a98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6182,7 +6182,7 @@ dependencies: "@types/node" "*" -"@types/http-proxy-middleware@*", "@types/http-proxy-middleware@^0.19.3": +"@types/http-proxy-middleware@^0.19.3": version "0.19.3" resolved "https://registry.npmjs.org/@types/http-proxy-middleware/-/http-proxy-middleware-0.19.3.tgz#b2eb96fbc0f9ac7250b5d9c4c53aade049497d03" integrity sha512-lnBTx6HCOUeIJMLbI/LaL5EmdKLhczJY5oeXZpX/cXE4rRqb3RmV7VcMpiEfYkmTjipv3h7IAyIINe4plEv7cA== @@ -7025,27 +7025,16 @@ resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== -"@types/webpack-dev-server@*": - version "3.11.1" - resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.1.tgz#f8f4dac1da226d530bd15a1d5dc34b23ba766ccb" - integrity sha512-rIb+LtUkKnh7+oIJm3WiMJONd71Q0lZuqGLcSqhZ5qjN9gV/CNmZe7Bai+brnBPZ/KVYOsr+4bFLiNZwjBicLw== +"@types/webpack-dev-server@*", "@types/webpack-dev-server@^3.11.0": + version "3.11.4" + resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07" + integrity sha512-DCKORHjqNNVuMIDWFrlljftvc9CL0+09p3l7lBpb8dRqgN5SmvkWCY4MPKxoI6wJgdRqohmoNbptkxqSKAzLRg== dependencies: "@types/connect-history-api-fallback" "*" "@types/express" "*" - "@types/http-proxy-middleware" "*" "@types/serve-static" "*" - "@types/webpack" "*" - -"@types/webpack-dev-server@3.11.0": - version "3.11.0" - resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#bcc3b85e7dc6ac2db25330610513f2228c2fcfb2" - integrity sha512-3+86AgSzl18n5P1iUP9/lz3G3GMztCp+wxdDvVuNhx1sr1jE79GpYfKHL8k+Vht3N74K2n98CuAEw4YPJCYtDA== - dependencies: - "@types/connect-history-api-fallback" "*" - "@types/express" "*" - "@types/http-proxy-middleware" "*" - "@types/serve-static" "*" - "@types/webpack" "*" + "@types/webpack" "^4" + http-proxy-middleware "^1.0.0" "@types/webpack-env@^1.15.2", "@types/webpack-env@^1.15.3": version "1.16.0" @@ -15181,6 +15170,17 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" +http-proxy-middleware@^1.0.0: + version "1.3.1" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz#43700d6d9eecb7419bf086a128d0f7205d9eb665" + integrity sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg== + dependencies: + "@types/http-proxy" "^1.17.5" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + http-proxy-middleware@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.0.tgz#20d1ac3409199c83e5d0383ba6436b04e7acb9fe" From 2e1fbe203be32e9fcf717ad483e8c7200c4d3df0 Mon Sep 17 00:00:00 2001 From: Anastasia Rodionova Date: Tue, 8 Jun 2021 19:45:13 +0200 Subject: [PATCH 016/206] Do not add / for html pages in rewriteDocLinks Signed-off-by: Anastasia Rodionova --- .changeset/seven-wolves-clean.md | 5 +++++ .../techdocs/src/reader/transformers/rewriteDocLinks.test.ts | 1 + plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/seven-wolves-clean.md diff --git a/.changeset/seven-wolves-clean.md b/.changeset/seven-wolves-clean.md new file mode 100644 index 0000000000..3b807b1af6 --- /dev/null +++ b/.changeset/seven-wolves-clean.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Do not add trailing slash for .html pages during doc links rewriting diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index 01467f665d..4ebd7411ac 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -82,6 +82,7 @@ describe('normalizeUrl', () => { ['http://example.org/folder#intro', 'http://example.org/folder/#intro'], ['http://example.org/folder/#intro', 'http://example.org/folder/#intro'], ['http://example.org/folder#', 'http://example.org/folder/#'], + ['http://example.org/page.html', 'http://example.org/page.html'], ])('should handle %s', (url, expected) => { expect(normalizeUrl(url)).toEqual(expected); }); diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts index 8c4bc29201..8d8fafa454 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts @@ -58,7 +58,7 @@ export const rewriteDocLinks = (): Transformer => { export function normalizeUrl(input: string): string { const url = new URL(input); - if (!url.pathname.endsWith('/')) { + if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { url.pathname += '/'; } From 14ce64b4fb338298577ac2772317a762d4de0d5f Mon Sep 17 00:00:00 2001 From: Anastasia Rodionova Date: Wed, 9 Jun 2021 18:08:01 +0200 Subject: [PATCH 017/206] Add pagination to ApisExplorerTable Signed-off-by: Anastasia Rodionova --- .changeset/shaggy-vans-travel.md | 5 +++++ .../src/components/ApiExplorerTable/ApiExplorerTable.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/shaggy-vans-travel.md diff --git a/.changeset/shaggy-vans-travel.md b/.changeset/shaggy-vans-travel.md new file mode 100644 index 0000000000..898731f584 --- /dev/null +++ b/.changeset/shaggy-vans-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Add pagination to ApiExplorerTable diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 5b9808e26d..78b5f1d240 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -199,7 +199,9 @@ export const ApiExplorerTable = ({ isLoading={loading} columns={columns} options={{ - paging: false, + paging: true, + pageSize: 20, + pageSizeOptions: [20, 50, 100], actionsColumnIndex: -1, loadingType: 'linear', padding: 'dense', From cc8e0b7478ada68e779172b7c5b36f930632fcd4 Mon Sep 17 00:00:00 2001 From: Daniel Ortega Date: Thu, 10 Jun 2021 00:50:17 +0200 Subject: [PATCH 018/206] #5986 Adding .DS_Store to Scaffolded Backstage app .gitignore file Signed-off-by: Daniel Ortega --- packages/create-app/templates/default-app/.gitignore.hbs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/create-app/templates/default-app/.gitignore.hbs b/packages/create-app/templates/default-app/.gitignore.hbs index 4adebc5adc..d16a8d3fba 100644 --- a/packages/create-app/templates/default-app/.gitignore.hbs +++ b/packages/create-app/templates/default-app/.gitignore.hbs @@ -1,3 +1,6 @@ +# macOS +.DS_Store + # Logs logs *.log From 772dbdb51145f891a21b35ee61b6a71a525c9d25 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Mon, 17 May 2021 18:16:06 +0100 Subject: [PATCH 019/206] feat: add database manager with per plugin config This commit introduces: - a new backwards compatible database manager which allows the end-user to set global and per plugin database configuration. - an early iteration of a database connector interface. - provides helper functions for each of the database connectors to meet the new interface. Signed-off-by: Minn Soe --- .changeset/five-donkeys-brake.md | 50 ++ .../tutorials/configuring-plugin-databases.md | 107 ++++ packages/backend-common/api-report.md | 459 ++++++++++-------- packages/backend-common/config.d.ts | 29 ++ .../src/database/PluginConnection.test.ts | 325 +++++++++++++ .../src/database/PluginConnection.ts | 160 ++++++ .../src/database/connection.test.ts | 51 +- .../backend-common/src/database/connection.ts | 101 +++- .../backend-common/src/database/connector.ts | 30 ++ packages/backend-common/src/database/index.ts | 1 + packages/backend-common/src/database/mysql.ts | 21 + .../backend-common/src/database/postgres.ts | 21 + .../backend-common/src/database/sqlite3.ts | 26 + packages/backend/src/index.ts | 4 +- 14 files changed, 1159 insertions(+), 226 deletions(-) create mode 100644 .changeset/five-donkeys-brake.md create mode 100644 docs/tutorials/configuring-plugin-databases.md create mode 100644 packages/backend-common/src/database/PluginConnection.test.ts create mode 100644 packages/backend-common/src/database/PluginConnection.ts create mode 100644 packages/backend-common/src/database/connector.ts diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md new file mode 100644 index 0000000000..c9db3112a9 --- /dev/null +++ b/.changeset/five-donkeys-brake.md @@ -0,0 +1,50 @@ +--- +'example-backend': minor +'@backstage/backend-common': minor +--- + +Introduces `PluginConnectionDatabaseManager`, a backwards compatible database +connection manager which allows developers to configure database connections on +a per plugin basis. + +The `backend.database` config path allows you to set `prefix` to use an +alternate prefix for automatically generated database names, the default is +`backstage_plugin_`. Use `backend.database.plugin.` to set plugin +specific database connection configuration, e.g. + +```yaml +backend: + database: + client: 'pg', + prefix: 'custom_prefix_' + connection: + host: 'localhost' + user: 'foo' + password: 'bar' + plugin: + catalog: + connection: + database: 'database_name_overriden' + scaffolder: + client: 'sqlite3' + connection: ':inmemory' +``` + +Existing backstage installations can be migrated by swapping out the database +manager under `packages/backend/src/index.ts` as shown below: + +```diff +import { +- SingleConnectionDatabaseManager, ++ PluginConnectionDatabaseManager, +} from '@backstage/backend-common'; + +// ... + +function makeCreateEnv(config: Config) { + // ... +- const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); ++ const databaseManager = PluginConnectionDatabaseManager.fromConfig(config); + // ... +} +``` diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md new file mode 100644 index 0000000000..b6281cb0ae --- /dev/null +++ b/docs/tutorials/configuring-plugin-databases.md @@ -0,0 +1,107 @@ +--- +id: configuring-plugin-databases +title: Configuring Plugin Specific Databases +# prettier-ignore +description: Guide on how to use predefined databases for each plugin. +--- + +There are occasions where it may be difficult to deploy Backstage with +automatically created databases in production due to access control or other +restrictions. For example, your infrastructure might be defined as code using +tools such as Terraform or AWS CloudFormation where the name of each database is +defined, created and assigned explicitly. + +`@backstage/backend-common` provides an alternate database manager which allows +you to set the client and database connection on a per plugin basis. This means +that you can do selectively run certain plugins in memory with `sqlite3`, set +different connection config including the name of the database and more. + +There are two additional configuration options for this database manager: + +- **`backend.database.prefix`:** is used to override the default + `backstage_plugin_` prefix which is used to generate a database name when it + is not explicitly set for that plugin. +- **`backend.database.plugin.`:** is used to define a `client` and + `connection` block for the plugin matching the `pluginId`, e.g. `catalog` is + the `pluginId` for the catalog plugin and any configuration defined under that + block is specific to that plugin. + +## Install Database Drivers + +If you intend to use both `postgres` and `sqlite3`, you need to make sure the +appropriate database drivers are installed in your `backend` package. + +```shell +cd packages/backend + +# install pg if you need postgres +yarn add pg + +# install sqlite3 if you intend to set it as the client +yarn add sqlite3 +``` + +## Add Configuration + +To override the default prefix, `backstage_plugin_`, set +`backend.database.prefix` as shown below. This will use databases such as +`my_company_catalog` and `my_company_auth` instead of `backstage_plugin_catalog` +and `backstage_plugin_auth`. + +```yaml +backend: + database: + client: pg + prefix: my_company_ + connection: + host: localhost + user: postgres + password: password + plugin: + code-coverage: + connection: + database: pg_code_coverage_set_by_user +``` + +In the example above, the `code-coverage` plugin will use the same connection +configuration defined under `database.connection` and use +`pg_code_coverage_set_by_user` instead of `my_company_code-coverage` which would +be automatically generated if a plugin configuration wasn't explicitly set. + +## Integrate `PluginConnectionDatabaseManager` into `backend` + +The `SingleConnectionDatabaseManager` used by default should be replaced with +the `PluginConnectionDatabaseManager` in your `packages/backend/src/index.ts` +file. Import the manager and replace the `.fromConfig` call as shown below: + +```diff +import { +- SingleConnectionDatabaseManager, ++ PluginConnectionDatabaseManager, +} from '@backstage/backend-common'; + +// ... + +function makeCreateEnv(config: Config) { + // ... +- const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); ++ const databaseManager = PluginConnectionDatabaseManager.fromConfig(config); + // ... +} +``` + +## Check Your Databases + +The `PluginConnectionDatabaseManager` preserves the behaviour of the +`SingleConnectionDatabaseManager`. If the database does not exist, it will +attempt to create it. You should ensure the databases that you configure exists +and that the connection details have the appropriate permissions to work with +each of the given databases if you are using this database manager to set the +database name upfront. If each database needs its own connection username, +password or host - you may set them under the plugin's `connection` block. + +`sqlite3` databases do not need to be created upfront as with the existing +database manager. + +Your Backstage App can now use different database clients and configuration per +plugin! diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 068220cad3..2648e7af13 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts - import { AzureIntegration } from '@backstage/integration'; import { BitbucketIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; @@ -31,95 +30,120 @@ import { Writable } from 'stream'; // @public (undocumented) export class AzureUrlReader implements UrlReader { - constructor(integration: AzureIntegration, deps: { - treeResponseFactory: ReadTreeResponseFactory; - }); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree(url: string, options?: ReadTreeOptions): Promise; - // (undocumented) - search(url: string, options?: SearchOptions): Promise; - // (undocumented) - toString(): string; + constructor( + integration: AzureIntegration, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, + ); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; } // @public export class BitbucketUrlReader implements UrlReader { - constructor(integration: BitbucketIntegration, deps: { - treeResponseFactory: ReadTreeResponseFactory; - }); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree(url: string, options?: ReadTreeOptions): Promise; - // (undocumented) - search(url: string, options?: SearchOptions): Promise; - // (undocumented) - toString(): string; + constructor( + integration: BitbucketIntegration, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, + ); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; } // @public export interface CacheClient { - delete(key: string): Promise; - get(key: string): Promise; - set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; + delete(key: string): Promise; + get(key: string): Promise; + set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; } // @public export class CacheManager { - forPlugin(pluginId: string): PluginCacheManager; - static fromConfig(config: Config, options?: CacheManagerOptions): CacheManager; - } + forPlugin(pluginId: string): PluginCacheManager; + static fromConfig(config: Config, options?: CacheManagerOptions): CacheManager; +} // @public (undocumented) export const coloredFormat: winston.Logform.Format; // @public (undocumented) export interface ContainerRunner { - // (undocumented) - runContainer(opts: RunContainerOptions): Promise; + // (undocumented) + runContainer(opts: RunContainerOptions): Promise; } // @public @deprecated export const createDatabase: typeof createDatabaseClient; // @public -export function createDatabaseClient(dbConfig: Config, overrides?: Partial): Knex; +export function createDatabaseClient( + dbConfig: Config, + overrides?: Partial, +): Knex; // @public (undocumented) -export function createRootLogger(options?: winston.LoggerOptions, env?: NodeJS.ProcessEnv): winston.Logger; +export function createRootLogger( + options?: winston.LoggerOptions, + env?: NodeJS.ProcessEnv, +): winston.Logger; // @public export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl; // @public (undocumented) -export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise; +export function createStatusCheckRouter( + options: StatusCheckRouterOptions, +): Promise; // @public (undocumented) export class DockerContainerRunner implements ContainerRunner { - constructor({ dockerClient }: { - dockerClient: Docker; - }); - // (undocumented) - runContainer({ imageName, command, args, logStream, mountDirs, workingDir, envVars, }: RunContainerOptions): Promise; + constructor({ dockerClient }: { dockerClient: Docker }); + // (undocumented) + runContainer({ + imageName, + command, + args, + logStream, + mountDirs, + workingDir, + envVars, + }: RunContainerOptions): Promise; } // @public -export function ensureDatabaseExists(dbConfig: Config, ...databases: Array): Promise; +export function ensureDatabaseExists( + dbConfig: Config, + ...databases: Array +): Promise; // @public -export function errorHandler(options?: ErrorHandlerOptions): ErrorRequestHandler; +export function errorHandler( + options?: ErrorHandlerOptions, +): ErrorRequestHandler; // @public (undocumented) export type ErrorHandlerOptions = { - showStackTraces?: boolean; - logger?: Logger; - logClientErrors?: boolean; + showStackTraces?: boolean; + logger?: Logger; + logClientErrors?: boolean; }; // @public (undocumented) @@ -130,120 +154,141 @@ export function getVoidLogger(): winston.Logger; // @public (undocumented) export class Git { - // (undocumented) - add({ dir, filepath, }: { - dir: string; - filepath: string; - }): Promise; - // (undocumented) - addRemote({ dir, url, remote, }: { - dir: string; - remote: string; - url: string; - }): Promise; - // (undocumented) - clone({ url, dir, ref, }: { - url: string; - dir: string; - ref?: string; - }): Promise; - // (undocumented) - commit({ dir, message, author, committer, }: { - dir: string; - message: string; - author: { - name: string; - email: string; - }; - committer: { - name: string; - email: string; - }; - }): Promise; - // (undocumented) - currentBranch({ dir, fullName, }: { - dir: string; - fullName?: boolean; - }): Promise; - // (undocumented) - fetch({ dir, remote, }: { - dir: string; - remote?: string; - }): Promise; - // (undocumented) - static fromAuth: ({ username, password, logger, }: { - username?: string | undefined; - password?: string | undefined; - logger?: Logger | undefined; - }) => Git; - // (undocumented) - init({ dir }: { - dir: string; - }): Promise; - // (undocumented) - merge({ dir, theirs, ours, author, committer, }: { - dir: string; - theirs: string; - ours?: string; - author: { - name: string; - email: string; - }; - committer: { - name: string; - email: string; - }; - }): Promise; - // (undocumented) - push({ dir, remote }: { - dir: string; - remote: string; - }): Promise; - // (undocumented) - readCommit({ dir, sha, }: { - dir: string; - sha: string; - }): Promise; - // (undocumented) - resolveRef({ dir, ref, }: { - dir: string; - ref: string; - }): Promise; + // (undocumented) + add({ dir, filepath }: { dir: string; filepath: string }): Promise; + // (undocumented) + addRemote({ + dir, + url, + remote, + }: { + dir: string; + remote: string; + url: string; + }): Promise; + // (undocumented) + clone({ + url, + dir, + ref, + }: { + url: string; + dir: string; + ref?: string; + }): Promise; + // (undocumented) + commit({ + dir, + message, + author, + committer, + }: { + dir: string; + message: string; + author: { + name: string; + email: string; + }; + committer: { + name: string; + email: string; + }; + }): Promise; + // (undocumented) + currentBranch({ + dir, + fullName, + }: { + dir: string; + fullName?: boolean; + }): Promise; + // (undocumented) + fetch({ dir, remote }: { dir: string; remote?: string }): Promise; + // (undocumented) + static fromAuth: ({ + username, + password, + logger, + }: { + username?: string | undefined; + password?: string | undefined; + logger?: Logger | undefined; + }) => Git; + // (undocumented) + init({ dir }: { dir: string }): Promise; + // (undocumented) + merge({ + dir, + theirs, + ours, + author, + committer, + }: { + dir: string; + theirs: string; + ours?: string; + author: { + name: string; + email: string; + }; + committer: { + name: string; + email: string; + }; + }): Promise; + // (undocumented) + push({ dir, remote }: { dir: string; remote: string }): Promise; + // (undocumented) + readCommit({ + dir, + sha, + }: { + dir: string; + sha: string; + }): Promise; + // (undocumented) + resolveRef({ dir, ref }: { dir: string; ref: string }): Promise; } // @public export class GithubUrlReader implements UrlReader { - constructor(integration: GitHubIntegration, deps: { - treeResponseFactory: ReadTreeResponseFactory; - credentialsProvider: GithubCredentialsProvider; - }); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree(url: string, options?: ReadTreeOptions): Promise; - // (undocumented) - search(url: string, options?: SearchOptions): Promise; - // (undocumented) - toString(): string; + constructor( + integration: GitHubIntegration, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + credentialsProvider: GithubCredentialsProvider; + }, + ); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; } // @public (undocumented) export class GitlabUrlReader implements UrlReader { - constructor(integration: GitLabIntegration, deps: { - treeResponseFactory: ReadTreeResponseFactory; - }); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree(url: string, options?: ReadTreeOptions): Promise; - // (undocumented) - search(url: string, options?: SearchOptions): Promise; - // (undocumented) - toString(): string; + constructor( + integration: GitLabIntegration, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, + ); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; } // @public @@ -254,32 +299,32 @@ export function notFoundHandler(): RequestHandler; // @public export type PluginCacheManager = { - getClient: (options?: ClientOptions) => CacheClient; + getClient: (options?: ClientOptions) => CacheClient; }; // @public export interface PluginDatabaseManager { - getClient(): Promise; + getClient(): Promise; } // @public export type PluginEndpointDiscovery = { - getBaseUrl(pluginId: string): Promise; - getExternalBaseUrl(pluginId: string): Promise; + getBaseUrl(pluginId: string): Promise; + getExternalBaseUrl(pluginId: string): Promise; }; // @public (undocumented) export type ReadTreeResponse = { - files(): Promise; - archive(): Promise; - dir(options?: ReadTreeResponseDirOptions): Promise; - etag: string; + files(): Promise; + archive(): Promise; + dir(options?: ReadTreeResponseDirOptions): Promise; + etag: string; }; // @public export type ReadTreeResponseFile = { - path: string; - content(): Promise; + path: string; + content(): Promise; }; // @public @@ -290,37 +335,37 @@ export function resolvePackagePath(name: string, ...paths: string[]): string; // @public (undocumented) export type RunContainerOptions = { - imageName: string; - command?: string | string[]; - args: string[]; - logStream?: Writable; - mountDirs?: Record; - workingDir?: string; - envVars?: Record; + imageName: string; + command?: string | string[]; + args: string[]; + logStream?: Writable; + mountDirs?: Record; + workingDir?: string; + envVars?: Record; }; // @public export type SearchResponse = { - files: SearchResponseFile[]; - etag: string; + files: SearchResponseFile[]; + etag: string; }; // @public export type SearchResponseFile = { - url: string; - content(): Promise; + url: string; + content(): Promise; }; // @public (undocumented) export type ServiceBuilder = { - loadConfig(config: ConfigReader): ServiceBuilder; - setPort(port: number): ServiceBuilder; - setHost(host: string): ServiceBuilder; - setLogger(logger: Logger): ServiceBuilder; - enableCors(options: cors.CorsOptions): ServiceBuilder; - setHttpsSettings(settings: HttpsSettings): ServiceBuilder; - addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; - start(): Promise; + loadConfig(config: ConfigReader): ServiceBuilder; + setPort(port: number): ServiceBuilder; + setHost(host: string): ServiceBuilder; + setLogger(logger: Logger): ServiceBuilder; + enableCors(options: cors.CorsOptions): ServiceBuilder; + setHttpsSettings(settings: HttpsSettings): ServiceBuilder; + addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; + start(): Promise; }; // @public (undocumented) @@ -328,52 +373,58 @@ export function setRootLogger(newLogger: winston.Logger): void; // @public export class SingleConnectionDatabaseManager { - forPlugin(pluginId: string): PluginDatabaseManager; - static fromConfig(config: Config): SingleConnectionDatabaseManager; - } + forPlugin(pluginId: string): PluginDatabaseManager; + static fromConfig(config: Config): SingleConnectionDatabaseManager; +} // @public export class SingleHostDiscovery implements PluginEndpointDiscovery { - static fromConfig(config: Config, options?: { - basePath?: string; - }): SingleHostDiscovery; - // (undocumented) - getBaseUrl(pluginId: string): Promise; - // (undocumented) - getExternalBaseUrl(pluginId: string): Promise; - } + static fromConfig( + config: Config, + options?: { + basePath?: string; + }, + ): SingleHostDiscovery; + // (undocumented) + getBaseUrl(pluginId: string): Promise; + // (undocumented) + getExternalBaseUrl(pluginId: string): Promise; +} // @public (undocumented) export type StatusCheck = () => Promise; // @public -export function statusCheckHandler(options?: StatusCheckHandlerOptions): Promise; +export function statusCheckHandler( + options?: StatusCheckHandlerOptions, +): Promise; // @public (undocumented) export interface StatusCheckHandlerOptions { - statusCheck?: StatusCheck; + statusCheck?: StatusCheck; } // @public export type UrlReader = { - read(url: string): Promise; - readTree(url: string, options?: ReadTreeOptions): Promise; - search(url: string, options?: SearchOptions): Promise; + read(url: string): Promise; + readTree(url: string, options?: ReadTreeOptions): Promise; + search(url: string, options?: SearchOptions): Promise; }; // @public export class UrlReaders { - static create({ logger, config, factories }: CreateOptions): UrlReader; - static default({ logger, config, factories }: CreateOptions): UrlReader; + static create({ logger, config, factories }: CreateOptions): UrlReader; + static default({ logger, config, factories }: CreateOptions): UrlReader; } // @public -export function useHotCleanup(_module: NodeModule, cancelEffect: () => void): void; +export function useHotCleanup( + _module: NodeModule, + cancelEffect: () => void, +): void; // @public export function useHotMemoize(_module: NodeModule, valueFactory: () => T): T; - // (No @packageDocumentation comment for this package) - ``` diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 875660a594..d735f39935 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -14,6 +14,23 @@ * limitations under the License. */ +export type PluginDatabaseConfig = + | { + /** Database client to use for plugin. */ + client?: 'sqlite3'; + /** Database connection to use with plugin. */ + connection?: ':memory:' | string | { filename: string }; + } + | { + /** Database client to use for plugin. */ + client?: 'pg'; + /** + * PostgreSQL connection string or knex configuration object for plugin. + * @secret + */ + connection?: string | object; + }; + export interface Config { app: { baseUrl: string; // defined in core, but repeated here without doc @@ -58,6 +75,12 @@ export interface Config { | { client: 'sqlite3'; connection: ':memory:' | string | { filename: string }; + /** Optional sqlite3 database filename prefix. */ + prefix?: string; + /** Override database config per plugin. */ + plugin?: { + [pluginId: string]: PluginDatabaseConfig; + }; } | { client: 'pg'; @@ -66,6 +89,12 @@ export interface Config { * @secret */ connection: string | object; + /** Optional PostgreSQL database prefix. */ + prefix?: string; + /** Override database config per plugin. */ + plugin?: { + [pluginId: string]: PluginDatabaseConfig; + }; }; /** Cache connection configuration, select cache type using the `store` field */ diff --git a/packages/backend-common/src/database/PluginConnection.test.ts b/packages/backend-common/src/database/PluginConnection.test.ts new file mode 100644 index 0000000000..e3a5157d7d --- /dev/null +++ b/packages/backend-common/src/database/PluginConnection.test.ts @@ -0,0 +1,325 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConfigReader } from '@backstage/config'; +import { omit } from 'lodash'; +import { createDatabaseClient, ensureDatabaseExists } from './connection'; +import { PluginConnectionDatabaseManager } from './PluginConnection'; + +jest.mock('./connection', () => ({ + ...jest.requireActual('./connection'), + createDatabaseClient: jest.fn(), + ensureDatabaseExists: jest.fn(), +})); + +describe('PluginConnectionDatabaseManager', () => { + // This is similar to the ts-jest `mocked` helper. + const mocked = (f: Function) => f as jest.Mock; + + afterEach(() => jest.resetAllMocks()); + + describe('PluginConnectionDatabaseManager.fromConfig', () => { + const backendConfig = { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', + }, + }, + }, + }; + const defaultConfig = () => new ConfigReader(backendConfig); + + it('accesses the backend.database key', () => { + const getConfig = jest.fn(); + const config = defaultConfig(); + config.getConfig = getConfig; + + PluginConnectionDatabaseManager.fromConfig(config); + + expect(getConfig.mock.calls[0][0]).toEqual('backend.database'); + }); + }); + + describe('PluginConnectionDatabaseManager.forPlugin', () => { + const config = { + backend: { + database: { + client: 'pg', + prefix: 'test_prefix_', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', + }, + plugin: { + testdbname: { + connection: { + database: 'database_name_overriden', + }, + }, + differentclient: { + client: 'sqlite3', + connection: { + filename: 'plugin_with_different_client', + }, + }, + differentclientconnstring: { + client: 'sqlite3', + connection: ':inmemory:', + }, + stringoverride: { + connection: 'postgresql://testuser:testpass@acme:5432/userdbname', + }, + }, + }, + }, + }; + const manager = PluginConnectionDatabaseManager.fromConfig( + new ConfigReader(config), + ); + + it('connects to a plugin database using default config', async () => { + const pluginId = 'pluginwithoutconfig'; + + await manager.forPlugin(pluginId).getClient(); + expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(1); + + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [baseConfig, overrides] = mockCalls[0]; + + // default config should be passed through to underlying connector + expect(baseConfig.get()).toMatchObject({ + client: 'pg', + connection: omit(config.backend.database.connection, ['database']), + }); + + // override using database name generated from pluginId and prefix + expect(overrides).toMatchObject({ + connection: { + database: `${config.backend.database.prefix}${pluginId}`, + }, + }); + }); + + it('provides a plugin db which uses components from top level connection string', async () => { + const testManager = PluginConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'pg', + connection: 'postgresql://foo:bar@acme:5432/foodb', + }, + }, + }), + ); + + await testManager.forPlugin('pluginwithoutconfig').getClient(); + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [baseConfig, overrides] = mockCalls[0]; + + // parsed connection string **without** db name should be passed through + expect(baseConfig.get()).toMatchObject({ + connection: { + host: 'acme', + user: 'foo', + password: 'bar', + port: '5432', + }, + }); + + // we expect a pg database name override with ${prefix} followed by pluginId + expect(overrides).toHaveProperty( + 'connection.database', + expect.stringContaining('pluginwithoutconfig'), + ); + }); + + it('uses top level sqlite database filename if plugin config is not present', async () => { + const testManager = PluginConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: 'some-file-path', + }, + }, + }), + ); + + await testManager.forPlugin('pluginwithoutconfig').getClient(); + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [_, overrides] = mockCalls[0]; + + expect(overrides).toHaveProperty( + 'connection.filename', + expect.stringContaining('some-file-path'), + ); + }); + + it('provides an inmemory sqlite database if top level is also inmemory and plugin config is not present', async () => { + const testManager = PluginConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':inmemory:', + }, + }, + }), + ); + + await testManager.forPlugin('pluginwithoutconfig').getClient(); + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [_, overrides] = mockCalls[0]; + + expect(overrides).toHaveProperty( + 'connection.filename', + expect.stringContaining(':inmemory:'), + ); + }); + + it('connects to a plugin database using a specific database name', async () => { + // testdbname.connection.database is set in config + await manager.forPlugin('testdbname').getClient(); + + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [_baseConfig, overrides] = mockCalls[0]; + + // simple case where only database name is overriden + expect(overrides).toMatchObject({ + connection: { + database: 'database_name_overriden', + }, + }); + }); + + it('ensure plugin specific database is created', async () => { + const pluginId = 'testdbname'; + // testdbname.connection.database is set in config + await manager.forPlugin(pluginId).getClient(); + + const mockCalls = mocked(ensureDatabaseExists).mock.calls.splice(-1); + const [_, dbname] = mockCalls[0]; + + expect(dbname).toEqual( + config.backend.database.plugin[pluginId].connection.database, + ); + }); + + it('provides different plugins with their own databases', async () => { + await manager.forPlugin('plugin1').getClient(); + await manager.forPlugin('plugin2').getClient(); + + expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(2); + + const mockCalls = mocked(createDatabaseClient).mock.calls; + const [plugin1CallArgs, plugin2CallArgs] = mockCalls; + + // database name overrides should be different + expect(plugin1CallArgs[1].connection.database).not.toEqual( + plugin2CallArgs[1].connection.database, + ); + }); + + it('uses plugin connection as base if default client is different from plugin client', async () => { + const pluginId = 'differentclient'; + await manager.forPlugin(pluginId).getClient(); + + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [baseConfig, _overrides] = mockCalls[0]; + + // plugin connection should be used as base config, client is different + expect(baseConfig.get()).toMatchObject({ + client: 'sqlite3', + connection: config.backend.database.plugin[pluginId].connection, + }); + }); + + it('provides database client specific base and override when client set under plugin', async () => { + const pluginId = 'differentclient'; + await manager.forPlugin(pluginId).getClient(); + + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [baseConfig, overrides] = mockCalls[0]; + + // plugin client should be sqlite3 + expect(baseConfig.get().client).toEqual('sqlite3'); + + // sqlite3 uses 'filename' instead of 'database' + expect(overrides).toHaveProperty('connection.filename'); + }); + + it('provides database client specific base from plugin connection string when client set under plugin', async () => { + const pluginId = 'differentclientconnstring'; + await manager.forPlugin(pluginId).getClient(); + + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [baseConfig, overrides] = mockCalls[0]; + + expect(baseConfig.get().client).toEqual('sqlite3'); + + expect(overrides).toHaveProperty('connection.filename', ':inmemory:'); + }); + + it('generates a database name override when prefix is not explicitly set', async () => { + const testManager = PluginConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', + }, + }, + }, + }), + ); + + await testManager.forPlugin('testplugin').getClient(); + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [_baseConfig, overrides] = mockCalls[0]; + + expect(overrides).toHaveProperty( + 'connection.database', + expect.stringContaining(PluginConnectionDatabaseManager.DEFAULT_PREFIX), + ); + }); + + it('uses values from plugin connection string if top level client should be used', async () => { + const pluginId = 'stringoverride'; + await manager.forPlugin(pluginId).getClient(); + + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [baseConfig, overrides] = mockCalls[0]; + + // plugin client should be pg + expect(baseConfig.get().client).toEqual('pg'); + + expect(overrides).toHaveProperty( + 'connection.database', + expect.stringContaining('userdbname'), + ); + }); + }); +}); diff --git a/packages/backend-common/src/database/PluginConnection.ts b/packages/backend-common/src/database/PluginConnection.ts new file mode 100644 index 0000000000..18c209c8d5 --- /dev/null +++ b/packages/backend-common/src/database/PluginConnection.ts @@ -0,0 +1,160 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Knex } from 'knex'; +import { omit } from 'lodash'; +import { Config, ConfigReader } from '@backstage/config'; +import { + createDatabaseClient, + ensureDatabaseExists, + createNameOverride, + normalizeConnection, +} from './connection'; +import { PluginDatabaseManager } from './types'; + +function pluginPath(pluginId: string): string { + return `plugin.${pluginId}`; +} + +export class PluginConnectionDatabaseManager { + static readonly DEFAULT_PREFIX = 'backstage_plugin_'; + + /** + * Creates a PluginConnectionDatabaseManager from `backend.database` config. + * + * The database manager allows the user to set connection and client settings on a per pluginId + * basis by defining a database config block under `plugin.` in addition to top level + * defaults. Optionally, a user may set `prefix` which is used to prefix generated database + * names if config is not provided. + * + * @param config The loaded application configuration. + */ + static fromConfig(config: Config): PluginConnectionDatabaseManager { + return new PluginConnectionDatabaseManager( + config.getConfig('backend.database'), + ); + } + + private constructor(private readonly config: Config) {} + + /** + * Generates a PluginDatabaseManager for consumption by plugins. + * + * @param pluginId The plugin that the database manager should be created for. Plugin names should be unique + * as they are used to look up database config overrides under `backend.database.plugin`. + */ + forPlugin(pluginId: string): PluginDatabaseManager { + const _this = this; + + return { + getClient(): Promise { + return _this.getDatabase(pluginId); + }, + }; + } + + /** + * Provides the canonical database name for a given pluginId. + * + * This method provides the effective database name which is determined using global + * and plugin specific database config. If no explicit database name is configured, + * this method will provide a generated name which is the pluginId prefixed using + * the value from `PluginConnectionDatabaseManager.DEFAULT_PREFIX`. + * + * @param pluginId Lookup the database name for given plugin + * */ + getDatabaseName(pluginId: string): string { + const pluginConfig: Config = this.getConfigForPlugin(pluginId); + + // determine root sqlite config to pass through as this is a special case + const rootConnection = this.config.get('connection'); + const rootSqliteName = + typeof rootConnection === 'string' + ? rootConnection + : this.config.getOptionalString('connection.filename') ?? ':inmemory:'; + + const prefix = + this.config.getOptionalString('prefix') ?? + PluginConnectionDatabaseManager.DEFAULT_PREFIX; + + const isSqlite = this.config.getString('client') === 'sqlite3'; + return ( + // attempt to lookup pg and mysql database name + pluginConfig.getOptionalString('connection.database') ?? + // attempt to lookup sqlite3 database file name + pluginConfig.getOptionalString('connection.filename') ?? + // if root is sqlite - attempt to use top level connection, fallback to :inmemory: + (isSqlite ? rootSqliteName : null) ?? + // generate a database name using prefix and pluginId + `${prefix}${pluginId}` + ); + } + + /** + * Provides a base database connector config by merging different config sources. + * + * This method provides a baseConfig for a database connector without the target + * database's name property ('database', 'filename'). The client type is determined + * by plugin specific config which uses the default as the fallback. + * + * If the client type is the same as the plugin or not specified, the global + * connection config will be extended with plugin specific config. + * + * @param pluginId The plugin that the database baseConfig should correspond to + * */ + private getConfigForPlugin(pluginId: string): Config { + const pluginConfig = this.config.getOptionalConfig(pluginPath(pluginId)); + + const baseClient = this.config.getString('client'); + const client = pluginConfig?.getOptionalString('client') ?? baseClient; + + const baseConnection = normalizeConnection( + this.config.get('connection'), + baseClient, + ); + const connection = normalizeConnection( + pluginConfig?.getOptional('connection') ?? {}, + client, + ); + + return new ConfigReader({ + client, + connection: { + // if same client type, extend original connection config without dbname config + ...(client === baseClient + ? omit(baseConnection, ['database', 'filename']) + : {}), + ...connection, + }, + }); + } + + private async getDatabase(pluginId: string): Promise { + const pluginConfig = this.getConfigForPlugin(pluginId); + + await ensureDatabaseExists(pluginConfig, this.getDatabaseName(pluginId)); + return createDatabaseClient( + pluginConfig, + this.getDatabaseOverrides(pluginId), + ); + } + + private getDatabaseOverrides(pluginId: string): Knex.Config { + return createNameOverride( + this.getConfigForPlugin(pluginId).get('client'), + this.getDatabaseName(pluginId), + ); + } +} diff --git a/packages/backend-common/src/database/connection.test.ts b/packages/backend-common/src/database/connection.test.ts index 6ab163ff7f..fa7ccd480a 100644 --- a/packages/backend-common/src/database/connection.test.ts +++ b/packages/backend-common/src/database/connection.test.ts @@ -15,7 +15,11 @@ */ import { ConfigReader } from '@backstage/config'; -import { createDatabaseClient } from './connection'; +import { + createDatabaseClient, + createNameOverride, + parseConnectionString, +} from './connection'; describe('database connection', () => { describe('createDatabaseClient', () => { @@ -103,4 +107,49 @@ describe('database connection', () => { ).toThrowError(); }); }); + + describe('createNameOverride', () => { + it('returns Knex config for postgres', () => { + expect(createNameOverride('pg', 'testpg')).toHaveProperty( + 'connection.database', + 'testpg', + ); + }); + + it('returns Knex config for sqlite', () => { + expect(createNameOverride('sqlite3', 'testsqlite')).toHaveProperty( + 'connection.filename', + 'testsqlite', + ); + }); + + it('returns Knex config for mysql', () => { + expect(createNameOverride('mysql', 'testmysql')).toHaveProperty( + 'connection.database', + 'testmysql', + ); + }); + + it('throws an error for unknown connection', () => { + expect(() => createNameOverride('unknown', 'testname')).toThrowError(); + }); + }); + + describe('parseConnectionString', () => { + it('returns parsed Knex.StaticConnectionConfig for postgres', () => { + expect( + parseConnectionString('postgresql://foo:bar@acme:5432/foodb', 'pg'), + ).toHaveProperty('database', 'foodb'); + }); + + it('returns parsed Knex.StaticConnectionConfig for mysql2', () => { + expect( + parseConnectionString('mysql://foo:bar@acme:3306/foodb', 'mysql2'), + ).toHaveProperty('database', 'foodb'); + }); + + it('throws an error if client hint is not provided', () => { + expect(() => parseConnectionString('sqlite://')).toThrow(); + }); + }); }); diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index fb16915cf4..4a4a8c6ba2 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -14,14 +14,30 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; +import { Config, JsonObject } from '@backstage/config'; +import { InputError } from '@backstage/errors'; import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; -import { createMysqlDatabaseClient, ensureMysqlDatabaseExists } from './mysql'; -import { createPgDatabaseClient, ensurePgDatabaseExists } from './postgres'; -import { createSqliteDatabaseClient } from './sqlite3'; +import { DatabaseConnector } from './connector'; -type DatabaseClient = 'pg' | 'sqlite3' | string; +import { mysqlConnector } from './mysql'; +import { pgConnector } from './postgres'; +import { sqlite3Connector } from './sqlite3'; + +type DatabaseClient = 'pg' | 'sqlite3' | 'mysql' | 'mysql2' | string; + +/** + * Mapping of client type to supported database connectors + * + * Database connectors can be aliased here, for example mysql2 uses + * the same connector as mysql. + * */ +const ConnectorMapping: Record = { + pg: pgConnector, + sqlite3: sqlite3Connector, + mysql: mysqlConnector, + mysql2: mysqlConnector, +}; /** * Creates a knex database connection @@ -35,15 +51,10 @@ export function createDatabaseClient( ) { const client: DatabaseClient = dbConfig.getString('client'); - if (client === 'pg') { - return createPgDatabaseClient(dbConfig, overrides); - } else if (client === 'mysql' || client === 'mysql2') { - return createMysqlDatabaseClient(dbConfig, overrides); - } else if (client === 'sqlite3') { - return createSqliteDatabaseClient(dbConfig, overrides); - } - - return knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides)); + return ( + ConnectorMapping[client]?.createClient(dbConfig, overrides) ?? + knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides)) + ); } /** @@ -61,11 +72,63 @@ export async function ensureDatabaseExists( ) { const client: DatabaseClient = dbConfig.getString('client'); - if (client === 'pg') { - return ensurePgDatabaseExists(dbConfig, ...databases); - } else if (client === 'mysql' || client === 'mysql2') { - return ensureMysqlDatabaseExists(dbConfig, ...databases); + return ConnectorMapping[client]?.ensureDatabaseExists?.( + dbConfig, + ...databases, + ); +} + +/** + * Provides a Knex.Config object with the provided database name for a given client. + * */ +export function createNameOverride( + client: string, + name: string, +): Partial { + try { + return ConnectorMapping[client].createNameOverride(name); + } catch (e) { + throw new InputError( + `Unable to create database name override for '${client}' connector`, + e, + ); + } +} + +/** + * Parses a connection string for a given client and provides a connection config. + * */ +export function parseConnectionString( + connectionString: string, + client?: string, +): Knex.StaticConnectionConfig { + if (typeof client === 'undefined' || client === null) { + throw new InputError( + 'Database connection string client type auto-detection is not yet supported.', + ); } - return undefined; + try { + return ConnectorMapping[client].parseConnectionString(connectionString); + } catch (e) { + throw new InputError( + `Unable to parse connection string for '${client}' connector`, + ); + } +} + +/** + * Normalizes a connection config or string into an object which can be passed to Knex. + * */ +export function normalizeConnection( + connection: Knex.StaticConnectionConfig | JsonObject | string, + client: string, +): Record { + if (typeof connection === 'undefined' || connection === null) { + return {}; + } + + return typeof connection === 'string' || connection instanceof String + ? parseConnectionString(connection as string, client) + : connection; } diff --git a/packages/backend-common/src/database/connector.ts b/packages/backend-common/src/database/connector.ts new file mode 100644 index 0000000000..0336510f19 --- /dev/null +++ b/packages/backend-common/src/database/connector.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Config } from '@backstage/config'; +import { Knex } from 'knex'; + +export interface DatabaseConnector { + createClient(dbConfig: Config, overrides?: Partial): Knex; + createNameOverride(name: string): Partial; + parseConnectionString( + connectionString: string, + client?: string, + ): Knex.StaticConnectionConfig; + ensureDatabaseExists?( + dbConfig: Config, + ...databases: Array + ): Promise; +} diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index c81153aa62..7a8a81e187 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -17,3 +17,4 @@ export * from './connection'; export * from './types'; export * from './SingleConnection'; +export * from './PluginConnection'; diff --git a/packages/backend-common/src/database/mysql.ts b/packages/backend-common/src/database/mysql.ts index be4632baf6..3bd874de11 100644 --- a/packages/backend-common/src/database/mysql.ts +++ b/packages/backend-common/src/database/mysql.ts @@ -18,6 +18,7 @@ import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; +import { DatabaseConnector } from './connector'; import yn from 'yn'; /** @@ -159,3 +160,23 @@ export async function ensureMysqlDatabaseExists( await admin.destroy(); } } + +export function createMysqlNameOverride(name: string): Partial { + return { + connection: { + database: name, + }, + }; +} + +/** + * MySql database connector. + * + * Exposes database connector functionality via an immutable object. + * */ +export const mysqlConnector: DatabaseConnector = Object.freeze({ + createClient: createMysqlDatabaseClient, + ensureDatabaseExists: ensureMysqlDatabaseExists, + createNameOverride: createMysqlNameOverride, + parseConnectionString: parseMysqlConnectionString, +}); diff --git a/packages/backend-common/src/database/postgres.ts b/packages/backend-common/src/database/postgres.ts index e05eab86e5..40c6a41504 100644 --- a/packages/backend-common/src/database/postgres.ts +++ b/packages/backend-common/src/database/postgres.ts @@ -17,6 +17,7 @@ import knexFactory, { Knex } from 'knex'; import { Config } from '@backstage/config'; import { mergeDatabaseConfig } from './config'; +import { DatabaseConnector } from './connector'; /** * Creates a knex postgres database connection @@ -131,3 +132,23 @@ export async function ensurePgDatabaseExists( await admin.destroy(); } } + +export function createPgNameOverride(name: string): Partial { + return { + connection: { + database: name, + }, + }; +} + +/** + * PostgreSQL database connector. + * + * Exposes database connector functionality via an immutable object. + * */ +export const pgConnector: DatabaseConnector = Object.freeze({ + createClient: createPgDatabaseClient, + ensureDatabaseExists: ensurePgDatabaseExists, + createNameOverride: createPgNameOverride, + parseConnectionString: parsePgConnectionString, +}); diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/sqlite3.ts index d4169e3899..98c39b9c97 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/sqlite3.ts @@ -19,6 +19,7 @@ import { ensureDirSync } from 'fs-extra'; import knexFactory, { Knex } from 'knex'; import path from 'path'; import { mergeDatabaseConfig } from './config'; +import { DatabaseConnector } from './connector'; /** * Creates a knex sqlite3 database connection @@ -99,3 +100,28 @@ export function buildSqliteDatabaseConfig( return config; } + +export function createSqliteNameOverride(name: string): Partial { + return { + connection: parseSqliteConnectionString(name), + }; +} + +export function parseSqliteConnectionString( + name: string, +): Knex.Sqlite3ConnectionConfig { + return { + filename: name, + }; +} + +/** + * Sqlite3 database connector. + * + * Exposes database connector functionality via an immutable object. + * */ +export const sqlite3Connector: DatabaseConnector = Object.freeze({ + createClient: createSqliteDatabaseClient, + createNameOverride: createSqliteNameOverride, + parseConnectionString: parseSqliteConnectionString, +}); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d43c8f0101..9cab64e797 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -29,7 +29,7 @@ import { getRootLogger, loadBackendConfig, notFoundHandler, - SingleConnectionDatabaseManager, + PluginConnectionDatabaseManager, SingleHostDiscovery, UrlReaders, useHotMemoize, @@ -59,7 +59,7 @@ function makeCreateEnv(config: Config) { root.info(`Created UrlReader ${reader}`); - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); + const databaseManager = PluginConnectionDatabaseManager.fromConfig(config); const cacheManager = CacheManager.fromConfig(config); return (plugin: string): PluginEnvironment => { From 4d17ececc53f76a9fe68991208523daf6ad42dc6 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Mon, 17 May 2021 19:38:57 +0100 Subject: [PATCH 020/206] docs: fix typo in db manager changeset and clarify Signed-off-by: Minn Soe --- .changeset/five-donkeys-brake.md | 2 +- .../tutorials/configuring-plugin-databases.md | 44 +++++++++++++------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md index c9db3112a9..15d2cf7381 100644 --- a/.changeset/five-donkeys-brake.md +++ b/.changeset/five-donkeys-brake.md @@ -27,7 +27,7 @@ backend: database: 'database_name_overriden' scaffolder: client: 'sqlite3' - connection: ':inmemory' + connection: ':inmemory:' ``` Existing backstage installations can be migrated by swapping out the database diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index b6281cb0ae..b65addd7a8 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -9,12 +9,19 @@ There are occasions where it may be difficult to deploy Backstage with automatically created databases in production due to access control or other restrictions. For example, your infrastructure might be defined as code using tools such as Terraform or AWS CloudFormation where the name of each database is -defined, created and assigned explicitly. +defined, created and assigned explicitly. You may also need to use different +credentials for each database or use a set of credentials without the +permissions needed to create databases. -`@backstage/backend-common` provides an alternate database manager which allows -you to set the client and database connection on a per plugin basis. This means -that you can do selectively run certain plugins in memory with `sqlite3`, set -different connection config including the name of the database and more. +`@backstage/backend-common` provides an alternate database manager, +`PluginConnectionDatabaseManager`, which allows the developer to set the client +and database connection on a per plugin basis in addition to the default client +and connection configuration. This means that you can use a `sqlite3` in memory +database for a specific plugin whilst using `postgres` for everything else and +so on. + +The database manager also allows you to change the database name prefix which is +used when a plugin database isn't explicitly configured. There are two additional configuration options for this database manager: @@ -41,12 +48,20 @@ yarn add pg yarn add sqlite3 ``` +From an operational perspective, you only need to install drivers for clients +that are actively used. + ## Add Configuration -To override the default prefix, `backstage_plugin_`, set -`backend.database.prefix` as shown below. This will use databases such as -`my_company_catalog` and `my_company_auth` instead of `backstage_plugin_catalog` -and `backstage_plugin_auth`. +You can set the same type of values for `backend.database..client` and +`backend.database..connection` which are also accepted at the top +level. + +It is possible to override the default database name prefix, +`backstage_plugin_`, which is used when a name isn't explicitly defined. Set +`backend.database.prefix` as shown below. The database names for plugins such as +`catalog` and `auth` would now be `my_company_catalog` and `my_company_auth` +instead of `backstage_plugin_catalog` and `backstage_plugin_auth`. ```yaml backend: @@ -94,11 +109,12 @@ function makeCreateEnv(config: Config) { The `PluginConnectionDatabaseManager` preserves the behaviour of the `SingleConnectionDatabaseManager`. If the database does not exist, it will -attempt to create it. You should ensure the databases that you configure exists -and that the connection details have the appropriate permissions to work with -each of the given databases if you are using this database manager to set the -database name upfront. If each database needs its own connection username, -password or host - you may set them under the plugin's `connection` block. +attempt to create it. + +If you are using this database manager to set the database name upfront because +the credentials do not have permissions to create databases, you must ensure +they exist before starting the service. The service will not be able to create +them, it can only use them. `sqlite3` databases do not need to be created upfront as with the existing database manager. From 630b75798a93f95b2f5fcf87e156b5d5c7d31ff4 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Mon, 17 May 2021 22:37:44 +0100 Subject: [PATCH 021/206] fix: return type of ensureDatabaseExists and run api-report Signed-off-by: Minn Soe --- packages/backend-common/api-report.md | 476 ++++++++---------- .../backend-common/src/database/connection.ts | 5 +- 2 files changed, 223 insertions(+), 258 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 2648e7af13..002cfaac89 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts + import { AzureIntegration } from '@backstage/integration'; import { BitbucketIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; @@ -15,6 +16,7 @@ import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegration } from '@backstage/integration'; import { GitLabIntegration } from '@backstage/integration'; import * as http from 'http'; +import { JsonObject } from '@backstage/config'; import { JsonValue } from '@backstage/config'; import { Knex } from 'knex'; import { Logger } from 'winston'; @@ -30,55 +32,49 @@ import { Writable } from 'stream'; // @public (undocumented) export class AzureUrlReader implements UrlReader { - constructor( - integration: AzureIntegration, - deps: { - treeResponseFactory: ReadTreeResponseFactory; - }, - ); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree(url: string, options?: ReadTreeOptions): Promise; - // (undocumented) - search(url: string, options?: SearchOptions): Promise; - // (undocumented) - toString(): string; + constructor(integration: AzureIntegration, deps: { + treeResponseFactory: ReadTreeResponseFactory; + }); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; } // @public export class BitbucketUrlReader implements UrlReader { - constructor( - integration: BitbucketIntegration, - deps: { - treeResponseFactory: ReadTreeResponseFactory; - }, - ); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree(url: string, options?: ReadTreeOptions): Promise; - // (undocumented) - search(url: string, options?: SearchOptions): Promise; - // (undocumented) - toString(): string; + constructor(integration: BitbucketIntegration, deps: { + treeResponseFactory: ReadTreeResponseFactory; + }); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; } // @public export interface CacheClient { - delete(key: string): Promise; - get(key: string): Promise; - set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; + delete(key: string): Promise; + get(key: string): Promise; + set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; } // @public export class CacheManager { - forPlugin(pluginId: string): PluginCacheManager; - static fromConfig(config: Config, options?: CacheManagerOptions): CacheManager; + forPlugin(pluginId: string): PluginCacheManager; + static fromConfig(config: Config, options?: CacheManagerOptions): CacheManager; } // @public (undocumented) @@ -86,64 +82,48 @@ export const coloredFormat: winston.Logform.Format; // @public (undocumented) export interface ContainerRunner { - // (undocumented) - runContainer(opts: RunContainerOptions): Promise; + // (undocumented) + runContainer(opts: RunContainerOptions): Promise; } // @public @deprecated export const createDatabase: typeof createDatabaseClient; // @public -export function createDatabaseClient( - dbConfig: Config, - overrides?: Partial, -): Knex; +export function createDatabaseClient(dbConfig: Config, overrides?: Partial): Knex; + +// @public +export function createNameOverride(client: string, name: string): Partial; // @public (undocumented) -export function createRootLogger( - options?: winston.LoggerOptions, - env?: NodeJS.ProcessEnv, -): winston.Logger; +export function createRootLogger(options?: winston.LoggerOptions, env?: NodeJS.ProcessEnv): winston.Logger; // @public export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl; // @public (undocumented) -export function createStatusCheckRouter( - options: StatusCheckRouterOptions, -): Promise; +export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise; // @public (undocumented) export class DockerContainerRunner implements ContainerRunner { - constructor({ dockerClient }: { dockerClient: Docker }); - // (undocumented) - runContainer({ - imageName, - command, - args, - logStream, - mountDirs, - workingDir, - envVars, - }: RunContainerOptions): Promise; + constructor({ dockerClient }: { + dockerClient: Docker; + }); + // (undocumented) + runContainer({ imageName, command, args, logStream, mountDirs, workingDir, envVars, }: RunContainerOptions): Promise; } // @public -export function ensureDatabaseExists( - dbConfig: Config, - ...databases: Array -): Promise; +export function ensureDatabaseExists(dbConfig: Config, ...databases: Array): Promise; // @public -export function errorHandler( - options?: ErrorHandlerOptions, -): ErrorRequestHandler; +export function errorHandler(options?: ErrorHandlerOptions): ErrorRequestHandler; // @public (undocumented) export type ErrorHandlerOptions = { - showStackTraces?: boolean; - logger?: Logger; - logClientErrors?: boolean; + showStackTraces?: boolean; + logger?: Logger; + logClientErrors?: boolean; }; // @public (undocumented) @@ -154,177 +134,171 @@ export function getVoidLogger(): winston.Logger; // @public (undocumented) export class Git { - // (undocumented) - add({ dir, filepath }: { dir: string; filepath: string }): Promise; - // (undocumented) - addRemote({ - dir, - url, - remote, - }: { - dir: string; - remote: string; - url: string; - }): Promise; - // (undocumented) - clone({ - url, - dir, - ref, - }: { - url: string; - dir: string; - ref?: string; - }): Promise; - // (undocumented) - commit({ - dir, - message, - author, - committer, - }: { - dir: string; - message: string; - author: { - name: string; - email: string; - }; - committer: { - name: string; - email: string; - }; - }): Promise; - // (undocumented) - currentBranch({ - dir, - fullName, - }: { - dir: string; - fullName?: boolean; - }): Promise; - // (undocumented) - fetch({ dir, remote }: { dir: string; remote?: string }): Promise; - // (undocumented) - static fromAuth: ({ - username, - password, - logger, - }: { - username?: string | undefined; - password?: string | undefined; - logger?: Logger | undefined; - }) => Git; - // (undocumented) - init({ dir }: { dir: string }): Promise; - // (undocumented) - merge({ - dir, - theirs, - ours, - author, - committer, - }: { - dir: string; - theirs: string; - ours?: string; - author: { - name: string; - email: string; - }; - committer: { - name: string; - email: string; - }; - }): Promise; - // (undocumented) - push({ dir, remote }: { dir: string; remote: string }): Promise; - // (undocumented) - readCommit({ - dir, - sha, - }: { - dir: string; - sha: string; - }): Promise; - // (undocumented) - resolveRef({ dir, ref }: { dir: string; ref: string }): Promise; + // (undocumented) + add({ dir, filepath, }: { + dir: string; + filepath: string; + }): Promise; + // (undocumented) + addRemote({ dir, url, remote, }: { + dir: string; + remote: string; + url: string; + }): Promise; + // (undocumented) + clone({ url, dir, ref, }: { + url: string; + dir: string; + ref?: string; + }): Promise; + // (undocumented) + commit({ dir, message, author, committer, }: { + dir: string; + message: string; + author: { + name: string; + email: string; + }; + committer: { + name: string; + email: string; + }; + }): Promise; + // (undocumented) + currentBranch({ dir, fullName, }: { + dir: string; + fullName?: boolean; + }): Promise; + // (undocumented) + fetch({ dir, remote, }: { + dir: string; + remote?: string; + }): Promise; + // (undocumented) + static fromAuth: ({ username, password, logger, }: { + username?: string | undefined; + password?: string | undefined; + logger?: Logger | undefined; + }) => Git; + // (undocumented) + init({ dir }: { + dir: string; + }): Promise; + // (undocumented) + merge({ dir, theirs, ours, author, committer, }: { + dir: string; + theirs: string; + ours?: string; + author: { + name: string; + email: string; + }; + committer: { + name: string; + email: string; + }; + }): Promise; + // (undocumented) + push({ dir, remote }: { + dir: string; + remote: string; + }): Promise; + // (undocumented) + readCommit({ dir, sha, }: { + dir: string; + sha: string; + }): Promise; + // (undocumented) + resolveRef({ dir, ref, }: { + dir: string; + ref: string; + }): Promise; } // @public export class GithubUrlReader implements UrlReader { - constructor( - integration: GitHubIntegration, - deps: { - treeResponseFactory: ReadTreeResponseFactory; - credentialsProvider: GithubCredentialsProvider; - }, - ); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree(url: string, options?: ReadTreeOptions): Promise; - // (undocumented) - search(url: string, options?: SearchOptions): Promise; - // (undocumented) - toString(): string; + constructor(integration: GitHubIntegration, deps: { + treeResponseFactory: ReadTreeResponseFactory; + credentialsProvider: GithubCredentialsProvider; + }); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; } // @public (undocumented) export class GitlabUrlReader implements UrlReader { - constructor( - integration: GitLabIntegration, - deps: { - treeResponseFactory: ReadTreeResponseFactory; - }, - ); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree(url: string, options?: ReadTreeOptions): Promise; - // (undocumented) - search(url: string, options?: SearchOptions): Promise; - // (undocumented) - toString(): string; + constructor(integration: GitLabIntegration, deps: { + treeResponseFactory: ReadTreeResponseFactory; + }); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; } // @public export function loadBackendConfig(options: Options): Promise; +// @public +export function normalizeConnection(connection: Knex.StaticConnectionConfig | JsonObject | string, client: string): Record; + // @public export function notFoundHandler(): RequestHandler; +// @public +export function parseConnectionString(connectionString: string, client?: string): Knex.StaticConnectionConfig; + // @public export type PluginCacheManager = { - getClient: (options?: ClientOptions) => CacheClient; + getClient: (options?: ClientOptions) => CacheClient; }; +// @public (undocumented) +export class PluginConnectionDatabaseManager { + // (undocumented) + static readonly DEFAULT_PREFIX = "backstage_plugin_"; + forPlugin(pluginId: string): PluginDatabaseManager; + static fromConfig(config: Config): PluginConnectionDatabaseManager; + getDatabaseName(pluginId: string): string; + } + // @public export interface PluginDatabaseManager { - getClient(): Promise; + getClient(): Promise; } // @public export type PluginEndpointDiscovery = { - getBaseUrl(pluginId: string): Promise; - getExternalBaseUrl(pluginId: string): Promise; + getBaseUrl(pluginId: string): Promise; + getExternalBaseUrl(pluginId: string): Promise; }; // @public (undocumented) export type ReadTreeResponse = { - files(): Promise; - archive(): Promise; - dir(options?: ReadTreeResponseDirOptions): Promise; - etag: string; + files(): Promise; + archive(): Promise; + dir(options?: ReadTreeResponseDirOptions): Promise; + etag: string; }; // @public export type ReadTreeResponseFile = { - path: string; - content(): Promise; + path: string; + content(): Promise; }; // @public @@ -335,37 +309,37 @@ export function resolvePackagePath(name: string, ...paths: string[]): string; // @public (undocumented) export type RunContainerOptions = { - imageName: string; - command?: string | string[]; - args: string[]; - logStream?: Writable; - mountDirs?: Record; - workingDir?: string; - envVars?: Record; + imageName: string; + command?: string | string[]; + args: string[]; + logStream?: Writable; + mountDirs?: Record; + workingDir?: string; + envVars?: Record; }; // @public export type SearchResponse = { - files: SearchResponseFile[]; - etag: string; + files: SearchResponseFile[]; + etag: string; }; // @public export type SearchResponseFile = { - url: string; - content(): Promise; + url: string; + content(): Promise; }; // @public (undocumented) export type ServiceBuilder = { - loadConfig(config: ConfigReader): ServiceBuilder; - setPort(port: number): ServiceBuilder; - setHost(host: string): ServiceBuilder; - setLogger(logger: Logger): ServiceBuilder; - enableCors(options: cors.CorsOptions): ServiceBuilder; - setHttpsSettings(settings: HttpsSettings): ServiceBuilder; - addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; - start(): Promise; + loadConfig(config: ConfigReader): ServiceBuilder; + setPort(port: number): ServiceBuilder; + setHost(host: string): ServiceBuilder; + setLogger(logger: Logger): ServiceBuilder; + enableCors(options: cors.CorsOptions): ServiceBuilder; + setHttpsSettings(settings: HttpsSettings): ServiceBuilder; + addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; + start(): Promise; }; // @public (undocumented) @@ -373,58 +347,52 @@ export function setRootLogger(newLogger: winston.Logger): void; // @public export class SingleConnectionDatabaseManager { - forPlugin(pluginId: string): PluginDatabaseManager; - static fromConfig(config: Config): SingleConnectionDatabaseManager; -} + forPlugin(pluginId: string): PluginDatabaseManager; + static fromConfig(config: Config): SingleConnectionDatabaseManager; + } // @public export class SingleHostDiscovery implements PluginEndpointDiscovery { - static fromConfig( - config: Config, - options?: { - basePath?: string; - }, - ): SingleHostDiscovery; - // (undocumented) - getBaseUrl(pluginId: string): Promise; - // (undocumented) - getExternalBaseUrl(pluginId: string): Promise; -} + static fromConfig(config: Config, options?: { + basePath?: string; + }): SingleHostDiscovery; + // (undocumented) + getBaseUrl(pluginId: string): Promise; + // (undocumented) + getExternalBaseUrl(pluginId: string): Promise; + } // @public (undocumented) export type StatusCheck = () => Promise; // @public -export function statusCheckHandler( - options?: StatusCheckHandlerOptions, -): Promise; +export function statusCheckHandler(options?: StatusCheckHandlerOptions): Promise; // @public (undocumented) export interface StatusCheckHandlerOptions { - statusCheck?: StatusCheck; + statusCheck?: StatusCheck; } // @public export type UrlReader = { - read(url: string): Promise; - readTree(url: string, options?: ReadTreeOptions): Promise; - search(url: string, options?: SearchOptions): Promise; + read(url: string): Promise; + readTree(url: string, options?: ReadTreeOptions): Promise; + search(url: string, options?: SearchOptions): Promise; }; // @public export class UrlReaders { - static create({ logger, config, factories }: CreateOptions): UrlReader; - static default({ logger, config, factories }: CreateOptions): UrlReader; + static create({ logger, config, factories }: CreateOptions): UrlReader; + static default({ logger, config, factories }: CreateOptions): UrlReader; } // @public -export function useHotCleanup( - _module: NodeModule, - cancelEffect: () => void, -): void; +export function useHotCleanup(_module: NodeModule, cancelEffect: () => void): void; // @public export function useHotMemoize(_module: NodeModule, valueFactory: () => T): T; + // (No @packageDocumentation comment for this package) + ``` diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 4a4a8c6ba2..cd92c3fcee 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -72,10 +72,7 @@ export async function ensureDatabaseExists( ) { const client: DatabaseClient = dbConfig.getString('client'); - return ConnectorMapping[client]?.ensureDatabaseExists?.( - dbConfig, - ...databases, - ); + ConnectorMapping[client]?.ensureDatabaseExists?.(dbConfig, ...databases); } /** From 4e0fdcb968bc14e0039f3b452e32e913fd874dc7 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Tue, 18 May 2021 23:13:03 +0100 Subject: [PATCH 022/206] test: add coverage for existing manager behaviour Signed-off-by: Minn Soe --- .../src/database/SingleConnection.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/database/SingleConnection.test.ts b/packages/backend-common/src/database/SingleConnection.test.ts index 3466b0f79d..231523c790 100644 --- a/packages/backend-common/src/database/SingleConnection.test.ts +++ b/packages/backend-common/src/database/SingleConnection.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { createDatabaseClient } from './connection'; +import { createDatabaseClient, ensureDatabaseExists } from './connection'; import { SingleConnectionDatabaseManager } from './SingleConnection'; jest.mock('./connection'); @@ -85,5 +85,13 @@ describe('SingleConnectionDatabaseManager', () => { plugin2CallArgs[1].connection.database, ); }); + + it('ensure plugin database is created', async () => { + await manager.forPlugin('test').getClient(); + const mockCalls = mocked(ensureDatabaseExists).mock.calls.splice(-1); + const [_, database] = mockCalls[0]; + + expect(database).toEqual('backstage_plugin_test'); + }); }); }); From 2ff816060d822cf9163e395367e29cecf76c77a6 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Tue, 18 May 2021 23:43:14 +0100 Subject: [PATCH 023/206] fix: ensure underlying connector promise passed up Signed-off-by: Minn Soe --- packages/backend-common/src/database/connection.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index cd92c3fcee..c9e0b3db48 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -69,10 +69,13 @@ export const createDatabase = createDatabaseClient; export async function ensureDatabaseExists( dbConfig: Config, ...databases: Array -) { +): Promise { const client: DatabaseClient = dbConfig.getString('client'); - ConnectorMapping[client]?.ensureDatabaseExists?.(dbConfig, ...databases); + return ConnectorMapping[client]?.ensureDatabaseExists?.( + dbConfig, + ...databases, + ); } /** From a9c4f0531faf3f7f2606d3b2d1fb5dc2d0f3ce32 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Wed, 19 May 2021 00:10:35 +0100 Subject: [PATCH 024/206] fix: sqlite3 memory typo and drop leading asterisk doc Signed-off-by: Minn Soe --- .changeset/five-donkeys-brake.md | 2 +- .../backend-common/src/database/PluginConnection.test.ts | 8 ++++---- packages/backend-common/src/database/PluginConnection.ts | 8 ++++---- packages/backend-common/src/database/connection.ts | 8 ++++---- packages/backend-common/src/database/mysql.ts | 2 +- packages/backend-common/src/database/postgres.ts | 2 +- packages/backend-common/src/database/sqlite3.ts | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md index 15d2cf7381..1e732b2b9a 100644 --- a/.changeset/five-donkeys-brake.md +++ b/.changeset/five-donkeys-brake.md @@ -27,7 +27,7 @@ backend: database: 'database_name_overriden' scaffolder: client: 'sqlite3' - connection: ':inmemory:' + connection: ':memory:' ``` Existing backstage installations can be migrated by swapping out the database diff --git a/packages/backend-common/src/database/PluginConnection.test.ts b/packages/backend-common/src/database/PluginConnection.test.ts index e3a5157d7d..51684210f8 100644 --- a/packages/backend-common/src/database/PluginConnection.test.ts +++ b/packages/backend-common/src/database/PluginConnection.test.ts @@ -83,7 +83,7 @@ describe('PluginConnectionDatabaseManager', () => { }, differentclientconnstring: { client: 'sqlite3', - connection: ':inmemory:', + connection: ':memory:', }, stringoverride: { connection: 'postgresql://testuser:testpass@acme:5432/userdbname', @@ -180,7 +180,7 @@ describe('PluginConnectionDatabaseManager', () => { backend: { database: { client: 'sqlite3', - connection: ':inmemory:', + connection: ':memory:', }, }, }), @@ -192,7 +192,7 @@ describe('PluginConnectionDatabaseManager', () => { expect(overrides).toHaveProperty( 'connection.filename', - expect.stringContaining(':inmemory:'), + expect.stringContaining(':memory:'), ); }); @@ -276,7 +276,7 @@ describe('PluginConnectionDatabaseManager', () => { expect(baseConfig.get().client).toEqual('sqlite3'); - expect(overrides).toHaveProperty('connection.filename', ':inmemory:'); + expect(overrides).toHaveProperty('connection.filename', ':memory:'); }); it('generates a database name override when prefix is not explicitly set', async () => { diff --git a/packages/backend-common/src/database/PluginConnection.ts b/packages/backend-common/src/database/PluginConnection.ts index 18c209c8d5..6db4e126b0 100644 --- a/packages/backend-common/src/database/PluginConnection.ts +++ b/packages/backend-common/src/database/PluginConnection.ts @@ -74,7 +74,7 @@ export class PluginConnectionDatabaseManager { * the value from `PluginConnectionDatabaseManager.DEFAULT_PREFIX`. * * @param pluginId Lookup the database name for given plugin - * */ + */ getDatabaseName(pluginId: string): string { const pluginConfig: Config = this.getConfigForPlugin(pluginId); @@ -83,7 +83,7 @@ export class PluginConnectionDatabaseManager { const rootSqliteName = typeof rootConnection === 'string' ? rootConnection - : this.config.getOptionalString('connection.filename') ?? ':inmemory:'; + : this.config.getOptionalString('connection.filename') ?? ':memory:'; const prefix = this.config.getOptionalString('prefix') ?? @@ -95,7 +95,7 @@ export class PluginConnectionDatabaseManager { pluginConfig.getOptionalString('connection.database') ?? // attempt to lookup sqlite3 database file name pluginConfig.getOptionalString('connection.filename') ?? - // if root is sqlite - attempt to use top level connection, fallback to :inmemory: + // if root is sqlite - attempt to use top level connection, fallback to :memory: (isSqlite ? rootSqliteName : null) ?? // generate a database name using prefix and pluginId `${prefix}${pluginId}` @@ -113,7 +113,7 @@ export class PluginConnectionDatabaseManager { * connection config will be extended with plugin specific config. * * @param pluginId The plugin that the database baseConfig should correspond to - * */ + */ private getConfigForPlugin(pluginId: string): Config { const pluginConfig = this.config.getOptionalConfig(pluginPath(pluginId)); diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index c9e0b3db48..005ed8819f 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -31,7 +31,7 @@ type DatabaseClient = 'pg' | 'sqlite3' | 'mysql' | 'mysql2' | string; * * Database connectors can be aliased here, for example mysql2 uses * the same connector as mysql. - * */ + */ const ConnectorMapping: Record = { pg: pgConnector, sqlite3: sqlite3Connector, @@ -80,7 +80,7 @@ export async function ensureDatabaseExists( /** * Provides a Knex.Config object with the provided database name for a given client. - * */ + */ export function createNameOverride( client: string, name: string, @@ -97,7 +97,7 @@ export function createNameOverride( /** * Parses a connection string for a given client and provides a connection config. - * */ + */ export function parseConnectionString( connectionString: string, client?: string, @@ -119,7 +119,7 @@ export function parseConnectionString( /** * Normalizes a connection config or string into an object which can be passed to Knex. - * */ + */ export function normalizeConnection( connection: Knex.StaticConnectionConfig | JsonObject | string, client: string, diff --git a/packages/backend-common/src/database/mysql.ts b/packages/backend-common/src/database/mysql.ts index 3bd874de11..beff5d9f82 100644 --- a/packages/backend-common/src/database/mysql.ts +++ b/packages/backend-common/src/database/mysql.ts @@ -173,7 +173,7 @@ export function createMysqlNameOverride(name: string): Partial { * MySql database connector. * * Exposes database connector functionality via an immutable object. - * */ + */ export const mysqlConnector: DatabaseConnector = Object.freeze({ createClient: createMysqlDatabaseClient, ensureDatabaseExists: ensureMysqlDatabaseExists, diff --git a/packages/backend-common/src/database/postgres.ts b/packages/backend-common/src/database/postgres.ts index 40c6a41504..000b84460c 100644 --- a/packages/backend-common/src/database/postgres.ts +++ b/packages/backend-common/src/database/postgres.ts @@ -145,7 +145,7 @@ export function createPgNameOverride(name: string): Partial { * PostgreSQL database connector. * * Exposes database connector functionality via an immutable object. - * */ + */ export const pgConnector: DatabaseConnector = Object.freeze({ createClient: createPgDatabaseClient, ensureDatabaseExists: ensurePgDatabaseExists, diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/sqlite3.ts index 98c39b9c97..83f5f60699 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/sqlite3.ts @@ -119,7 +119,7 @@ export function parseSqliteConnectionString( * Sqlite3 database connector. * * Exposes database connector functionality via an immutable object. - * */ + */ export const sqlite3Connector: DatabaseConnector = Object.freeze({ createClient: createSqliteDatabaseClient, createNameOverride: createSqliteNameOverride, From ef1d705b76d0732316fda91789f8c48b98c2c718 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Wed, 19 May 2021 00:22:44 +0100 Subject: [PATCH 025/206] fix: make plugin database getDatabaseName private Signed-off-by: Minn Soe --- packages/backend-common/api-report.md | 1 - packages/backend-common/src/database/PluginConnection.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 002cfaac89..b5e4b0a84e 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -273,7 +273,6 @@ export class PluginConnectionDatabaseManager { static readonly DEFAULT_PREFIX = "backstage_plugin_"; forPlugin(pluginId: string): PluginDatabaseManager; static fromConfig(config: Config): PluginConnectionDatabaseManager; - getDatabaseName(pluginId: string): string; } // @public diff --git a/packages/backend-common/src/database/PluginConnection.ts b/packages/backend-common/src/database/PluginConnection.ts index 6db4e126b0..4f41bd068e 100644 --- a/packages/backend-common/src/database/PluginConnection.ts +++ b/packages/backend-common/src/database/PluginConnection.ts @@ -75,7 +75,7 @@ export class PluginConnectionDatabaseManager { * * @param pluginId Lookup the database name for given plugin */ - getDatabaseName(pluginId: string): string { + private getDatabaseName(pluginId: string): string { const pluginConfig: Config = this.getConfigForPlugin(pluginId); // determine root sqlite config to pass through as this is a special case From 2976f3bae349bb37de3f20d8ae2a1cc8456f9ead Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Tue, 25 May 2021 18:05:21 +0100 Subject: [PATCH 026/206] refactor: rename and deprecate database manager Changes: - Deprecates SingleConnectionManager and aliases to DatabaseManager. - Simplifies database config typing in `config.d.ts`. - Drops implementation specific assert in SingleConnectionManager test. - Move database prefix reference and drop static property for default value. Signed-off-by: Minn Soe --- .changeset/five-donkeys-brake.md | 15 ++--- packages/backend-common/api-report.md | 21 +++--- packages/backend-common/config.d.ts | 67 ++++++++----------- ...ection.test.ts => DatabaseManager.test.ts} | 57 +++++++--------- ...PluginConnection.ts => DatabaseManager.ts} | 30 ++++----- .../src/database/SingleConnection.test.ts | 10 +-- .../src/database/SingleConnection.ts | 63 ++--------------- packages/backend-common/src/database/index.ts | 2 +- packages/backend/src/index.ts | 4 +- 9 files changed, 97 insertions(+), 172 deletions(-) rename packages/backend-common/src/database/{PluginConnection.test.ts => DatabaseManager.test.ts} (88%) rename packages/backend-common/src/database/{PluginConnection.ts => DatabaseManager.ts} (88%) diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md index 1e732b2b9a..d5bc695714 100644 --- a/.changeset/five-donkeys-brake.md +++ b/.changeset/five-donkeys-brake.md @@ -1,11 +1,10 @@ --- -'example-backend': minor '@backstage/backend-common': minor --- -Introduces `PluginConnectionDatabaseManager`, a backwards compatible database -connection manager which allows developers to configure database connections on -a per plugin basis. +Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database +connection manager, `DatabaseManager`, which allows developers to configure database +connections on a per plugin basis. The `backend.database` config path allows you to set `prefix` to use an alternate prefix for automatically generated database names, the default is @@ -30,13 +29,13 @@ backend: connection: ':memory:' ``` -Existing backstage installations can be migrated by swapping out the database -manager under `packages/backend/src/index.ts` as shown below: +Migrate existing backstage installations by swapping out the database manager in the +`packages/backend/src/index.ts` file as shown below: ```diff import { - SingleConnectionDatabaseManager, -+ PluginConnectionDatabaseManager, ++ DatabaseManager, } from '@backstage/backend-common'; // ... @@ -44,7 +43,7 @@ import { function makeCreateEnv(config: Config) { // ... - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); -+ const databaseManager = PluginConnectionDatabaseManager.fromConfig(config); ++ const databaseManager = DatabaseManager.fromConfig(config); // ... } ``` diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index b5e4b0a84e..d4d075004a 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -104,6 +104,12 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl; // @public (undocumented) export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise; +// @public (undocumented) +export class DatabaseManager { + forPlugin(pluginId: string): PluginDatabaseManager; + static fromConfig(config: Config): DatabaseManager; + } + // @public (undocumented) export class DockerContainerRunner implements ContainerRunner { constructor({ dockerClient }: { @@ -267,14 +273,6 @@ export type PluginCacheManager = { getClient: (options?: ClientOptions) => CacheClient; }; -// @public (undocumented) -export class PluginConnectionDatabaseManager { - // (undocumented) - static readonly DEFAULT_PREFIX = "backstage_plugin_"; - forPlugin(pluginId: string): PluginDatabaseManager; - static fromConfig(config: Config): PluginConnectionDatabaseManager; - } - // @public export interface PluginDatabaseManager { getClient(): Promise; @@ -344,11 +342,8 @@ export type ServiceBuilder = { // @public (undocumented) export function setRootLogger(newLogger: winston.Logger): void; -// @public -export class SingleConnectionDatabaseManager { - forPlugin(pluginId: string): PluginDatabaseManager; - static fromConfig(config: Config): SingleConnectionDatabaseManager; - } +// @public @deprecated +export const SingleConnectionDatabaseManager: typeof DatabaseManager; // @public export class SingleHostDiscovery implements PluginEndpointDiscovery { diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index d735f39935..3ada25dd8a 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -14,22 +14,15 @@ * limitations under the License. */ -export type PluginDatabaseConfig = - | { - /** Database client to use for plugin. */ - client?: 'sqlite3'; - /** Database connection to use with plugin. */ - connection?: ':memory:' | string | { filename: string }; - } - | { - /** Database client to use for plugin. */ - client?: 'pg'; - /** - * PostgreSQL connection string or knex configuration object for plugin. - * @secret - */ - connection?: string | object; - }; +export type PluginDatabaseConfig = { + /** Database client to use. */ + client?: 'sqlite3' | 'pg'; + /** + * Database connection to use. + * @secret + */ + connection?: string | object; +}; export interface Config { app: { @@ -70,32 +63,30 @@ export interface Config { }; }; - /** Database connection configuration, select database type using the `client` field */ - database: - | { - client: 'sqlite3'; - connection: ':memory:' | string | { filename: string }; - /** Optional sqlite3 database filename prefix. */ - prefix?: string; - /** Override database config per plugin. */ - plugin?: { - [pluginId: string]: PluginDatabaseConfig; - }; - } - | { - client: 'pg'; + /** Database connection configuration, select base database type using the `client` field */ + database: { + /** Default database client to use */ + client: 'sqlite3' | 'pg'; + /** + * Base database connection string or Knex object + * @secret + */ + connection: string | object; + /** Database name prefix override */ + prefix?: string; + /** Plugin specific database configuration and client override */ + plugin?: { + [pluginId: string]: { + /** Database client override */ + client?: 'sqlite3' | 'pg'; /** - * PostgreSQL connection string or knex configuration object. + * Database connection string or Knex object override * @secret */ - connection: string | object; - /** Optional PostgreSQL database prefix. */ - prefix?: string; - /** Override database config per plugin. */ - plugin?: { - [pluginId: string]: PluginDatabaseConfig; - }; + connection?: string | object; }; + }; + }; /** Cache connection configuration, select cache type using the `store` field */ cache?: diff --git a/packages/backend-common/src/database/PluginConnection.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts similarity index 88% rename from packages/backend-common/src/database/PluginConnection.test.ts rename to packages/backend-common/src/database/DatabaseManager.test.ts index 51684210f8..8f3331403d 100644 --- a/packages/backend-common/src/database/PluginConnection.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { omit } from 'lodash'; import { createDatabaseClient, ensureDatabaseExists } from './connection'; -import { PluginConnectionDatabaseManager } from './PluginConnection'; +import { DatabaseManager } from './DatabaseManager'; jest.mock('./connection', () => ({ ...jest.requireActual('./connection'), @@ -24,40 +24,35 @@ jest.mock('./connection', () => ({ ensureDatabaseExists: jest.fn(), })); -describe('PluginConnectionDatabaseManager', () => { +describe('DatabaseManager', () => { // This is similar to the ts-jest `mocked` helper. const mocked = (f: Function) => f as jest.Mock; afterEach(() => jest.resetAllMocks()); - describe('PluginConnectionDatabaseManager.fromConfig', () => { - const backendConfig = { - backend: { - database: { - client: 'pg', - connection: { - host: 'localhost', - user: 'foo', - password: 'bar', - database: 'foodb', + describe('DatabaseManager.fromConfig', () => { + it('accesses the backend.database key', () => { + const config = new ConfigReader({ + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', + }, }, }, - }, - }; - const defaultConfig = () => new ConfigReader(backendConfig); + }); + const getConfigSpy = jest.spyOn(config, 'getConfig'); + DatabaseManager.fromConfig(config); - it('accesses the backend.database key', () => { - const getConfig = jest.fn(); - const config = defaultConfig(); - config.getConfig = getConfig; - - PluginConnectionDatabaseManager.fromConfig(config); - - expect(getConfig.mock.calls[0][0]).toEqual('backend.database'); + expect(getConfigSpy).toHaveBeenCalledWith('backend.database'); }); }); - describe('PluginConnectionDatabaseManager.forPlugin', () => { + describe('DatabaseManager.forPlugin', () => { const config = { backend: { database: { @@ -92,9 +87,7 @@ describe('PluginConnectionDatabaseManager', () => { }, }, }; - const manager = PluginConnectionDatabaseManager.fromConfig( - new ConfigReader(config), - ); + const manager = DatabaseManager.fromConfig(new ConfigReader(config)); it('connects to a plugin database using default config', async () => { const pluginId = 'pluginwithoutconfig'; @@ -120,7 +113,7 @@ describe('PluginConnectionDatabaseManager', () => { }); it('provides a plugin db which uses components from top level connection string', async () => { - const testManager = PluginConnectionDatabaseManager.fromConfig( + const testManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -153,7 +146,7 @@ describe('PluginConnectionDatabaseManager', () => { }); it('uses top level sqlite database filename if plugin config is not present', async () => { - const testManager = PluginConnectionDatabaseManager.fromConfig( + const testManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -175,7 +168,7 @@ describe('PluginConnectionDatabaseManager', () => { }); it('provides an inmemory sqlite database if top level is also inmemory and plugin config is not present', async () => { - const testManager = PluginConnectionDatabaseManager.fromConfig( + const testManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -280,7 +273,7 @@ describe('PluginConnectionDatabaseManager', () => { }); it('generates a database name override when prefix is not explicitly set', async () => { - const testManager = PluginConnectionDatabaseManager.fromConfig( + const testManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -302,7 +295,7 @@ describe('PluginConnectionDatabaseManager', () => { expect(overrides).toHaveProperty( 'connection.database', - expect.stringContaining(PluginConnectionDatabaseManager.DEFAULT_PREFIX), + expect.stringContaining('backstage_plugin_'), ); }); diff --git a/packages/backend-common/src/database/PluginConnection.ts b/packages/backend-common/src/database/DatabaseManager.ts similarity index 88% rename from packages/backend-common/src/database/PluginConnection.ts rename to packages/backend-common/src/database/DatabaseManager.ts index 4f41bd068e..34ceea55e3 100644 --- a/packages/backend-common/src/database/PluginConnection.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -28,11 +28,9 @@ function pluginPath(pluginId: string): string { return `plugin.${pluginId}`; } -export class PluginConnectionDatabaseManager { - static readonly DEFAULT_PREFIX = 'backstage_plugin_'; - +export class DatabaseManager { /** - * Creates a PluginConnectionDatabaseManager from `backend.database` config. + * Creates a DatabaseManager from `backend.database` config. * * The database manager allows the user to set connection and client settings on a per pluginId * basis by defining a database config block under `plugin.` in addition to top level @@ -41,13 +39,19 @@ export class PluginConnectionDatabaseManager { * * @param config The loaded application configuration. */ - static fromConfig(config: Config): PluginConnectionDatabaseManager { - return new PluginConnectionDatabaseManager( - config.getConfig('backend.database'), + static fromConfig(config: Config): DatabaseManager { + const databaseConfig = config.getConfig('backend.database'); + + return new DatabaseManager( + databaseConfig, + databaseConfig.getOptionalString('prefix'), ); } - private constructor(private readonly config: Config) {} + private constructor( + private readonly config: Config, + private readonly prefix: string = 'backstage_plugin_', + ) {} /** * Generates a PluginDatabaseManager for consumption by plugins. @@ -70,8 +74,8 @@ export class PluginConnectionDatabaseManager { * * This method provides the effective database name which is determined using global * and plugin specific database config. If no explicit database name is configured, - * this method will provide a generated name which is the pluginId prefixed using - * the value from `PluginConnectionDatabaseManager.DEFAULT_PREFIX`. + * this method will provide a generated name which is the pluginId prefixed with + * 'backstage_plugin_'. * * @param pluginId Lookup the database name for given plugin */ @@ -85,10 +89,6 @@ export class PluginConnectionDatabaseManager { ? rootConnection : this.config.getOptionalString('connection.filename') ?? ':memory:'; - const prefix = - this.config.getOptionalString('prefix') ?? - PluginConnectionDatabaseManager.DEFAULT_PREFIX; - const isSqlite = this.config.getString('client') === 'sqlite3'; return ( // attempt to lookup pg and mysql database name @@ -98,7 +98,7 @@ export class PluginConnectionDatabaseManager { // if root is sqlite - attempt to use top level connection, fallback to :memory: (isSqlite ? rootSqliteName : null) ?? // generate a database name using prefix and pluginId - `${prefix}${pluginId}` + `${this.prefix}${pluginId}` ); } diff --git a/packages/backend-common/src/database/SingleConnection.test.ts b/packages/backend-common/src/database/SingleConnection.test.ts index 231523c790..852dd33944 100644 --- a/packages/backend-common/src/database/SingleConnection.test.ts +++ b/packages/backend-common/src/database/SingleConnection.test.ts @@ -18,7 +18,11 @@ import { ConfigReader } from '@backstage/config'; import { createDatabaseClient, ensureDatabaseExists } from './connection'; import { SingleConnectionDatabaseManager } from './SingleConnection'; -jest.mock('./connection'); +jest.mock('./connection', () => ({ + ...jest.requireActual('./connection'), + createDatabaseClient: jest.fn(), + ensureDatabaseExists: jest.fn(), +})); describe('SingleConnectionDatabaseManager', () => { const defaultConfigOptions = { @@ -43,9 +47,8 @@ describe('SingleConnectionDatabaseManager', () => { describe('SingleConnectionDatabaseManager.fromConfig', () => { it('accesses the backend.database key', () => { - const getConfig = jest.fn(); const config = defaultConfig(); - config.getConfig = getConfig; + const getConfig = jest.spyOn(config, 'getConfig'); SingleConnectionDatabaseManager.fromConfig(config); @@ -64,7 +67,6 @@ describe('SingleConnectionDatabaseManager', () => { const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); const callArgs = mockCalls[0]; - expect(callArgs[0].get()).toEqual(defaultConfigOptions.backend.database); expect(callArgs[1].connection.database).toEqual( `backstage_plugin_${pluginId}`, ); diff --git a/packages/backend-common/src/database/SingleConnection.ts b/packages/backend-common/src/database/SingleConnection.ts index 1c5931a662..5bdd99c0ae 100644 --- a/packages/backend-common/src/database/SingleConnection.ts +++ b/packages/backend-common/src/database/SingleConnection.ts @@ -14,69 +14,14 @@ * limitations under the License. */ -import { Knex } from 'knex'; -import { Config } from '@backstage/config'; -import { createDatabaseClient, ensureDatabaseExists } from './connection'; -import { PluginDatabaseManager } from './types'; +import { DatabaseManager } from './DatabaseManager'; /** * Implements a Database Manager which will automatically create new databases * for plugins when requested. All requested databases are created with the * credentials provided; if the database already exists no attempt to create * the database will be made. + * + * @deprecated Use `DatabaseManager` from `@backend-common` instead. */ -export class SingleConnectionDatabaseManager { - /** - * Creates a new SingleConnectionDatabaseManager instance by reading from the `backend` - * config section, specifically the `.database` key for discovering the management - * database configuration. - * - * @param config The loaded application configuration. - */ - static fromConfig(config: Config): SingleConnectionDatabaseManager { - return new SingleConnectionDatabaseManager( - config.getConfig('backend.database'), - ); - } - - private constructor(private readonly config: Config) {} - - /** - * Generates a PluginDatabaseManager for consumption by plugins. - * - * @param pluginId The plugin that the database manager should be created for. Plugin names should be unique. - */ - forPlugin(pluginId: string): PluginDatabaseManager { - const _this = this; - - return { - getClient(): Promise { - return _this.getDatabase(pluginId); - }, - }; - } - - private async getDatabase(pluginId: string): Promise { - const config = this.config; - const overrides = SingleConnectionDatabaseManager.getDatabaseOverrides( - pluginId, - ); - const overrideConfig = overrides.connection as Knex.ConnectionConfig; - await this.ensureDatabase(overrideConfig.database); - - return createDatabaseClient(config, overrides); - } - - private static getDatabaseOverrides(pluginId: string): Knex.Config { - return { - connection: { - database: `backstage_plugin_${pluginId}`, - }, - }; - } - - private async ensureDatabase(database: string) { - const config = this.config; - await ensureDatabaseExists(config, database); - } -} +export const SingleConnectionDatabaseManager = DatabaseManager; diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 7a8a81e187..7dfb08359b 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -17,4 +17,4 @@ export * from './connection'; export * from './types'; export * from './SingleConnection'; -export * from './PluginConnection'; +export * from './DatabaseManager'; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9cab64e797..67149c8163 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -29,7 +29,7 @@ import { getRootLogger, loadBackendConfig, notFoundHandler, - PluginConnectionDatabaseManager, + DatabaseManager, SingleHostDiscovery, UrlReaders, useHotMemoize, @@ -59,7 +59,7 @@ function makeCreateEnv(config: Config) { root.info(`Created UrlReader ${reader}`); - const databaseManager = PluginConnectionDatabaseManager.fromConfig(config); + const databaseManager = DatabaseManager.fromConfig(config); const cacheManager = CacheManager.fromConfig(config); return (plugin: string): PluginEnvironment => { From 5a3ce340728f4bc51aa58fc688db71f38f5e155e Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Tue, 25 May 2021 18:28:05 +0100 Subject: [PATCH 027/206] refactor: migrate from deprecated database manager Changes: - Swaps out `SingleConnectionDatabaseManager` to `DatabaseManager` across the repo. - Updates `backend-test-utils` to generate test plugin names prefixed with db to satisfy plugin naming constraint, e.g. 0 becomes db0. Signed-off-by: Minn Soe --- .changeset/five-donkeys-brake.md | 1 + .../tutorials/configuring-plugin-databases.md | 222 +++++++++++------- packages/backend-common/config.d.ts | 10 - .../src/database/TestDatabases.test.ts | 6 +- .../src/database/TestDatabases.ts | 12 +- .../backend-test-utils/src/database/types.ts | 5 +- .../default-app/packages/backend/src/index.ts | 4 +- .../src/service/CodeCoverageDatabase.test.ts | 4 +- .../src/service/router.test.ts | 4 +- .../tasks/StorageTaskBroker.test.ts | 7 +- .../src/scaffolder/tasks/TaskWorker.test.ts | 9 +- .../src/service/router.test.ts | 4 +- 12 files changed, 168 insertions(+), 120 deletions(-) diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md index d5bc695714..8a6523bb61 100644 --- a/.changeset/five-donkeys-brake.md +++ b/.changeset/five-donkeys-brake.md @@ -1,5 +1,6 @@ --- '@backstage/backend-common': minor +'@backstage/create-app': minor --- Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index b65addd7a8..27b800b3d2 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -1,42 +1,40 @@ --- id: configuring-plugin-databases -title: Configuring Plugin Specific Databases +title: Configuring Plugin Databases # prettier-ignore -description: Guide on how to use predefined databases for each plugin. +description: Guide on how to configure Backstage databases. --- -There are occasions where it may be difficult to deploy Backstage with -automatically created databases in production due to access control or other -restrictions. For example, your infrastructure might be defined as code using -tools such as Terraform or AWS CloudFormation where the name of each database is -defined, created and assigned explicitly. You may also need to use different -credentials for each database or use a set of credentials without the -permissions needed to create databases. +This guide covers a variety of production persistence use cases which are +supported out of the box by Backstage. The database manager allows the developer +to set the client and database connection details on a per plugin basis in +addition to the base client and connection configuration. This means that you +can use a SQLite 3 in-memory database for a specific plugin whilst using +PostgreSQL for everything else and so on. -`@backstage/backend-common` provides an alternate database manager, -`PluginConnectionDatabaseManager`, which allows the developer to set the client -and database connection on a per plugin basis in addition to the default client -and connection configuration. This means that you can use a `sqlite3` in memory -database for a specific plugin whilst using `postgres` for everything else and -so on. +By default, Backstage uses automatically created databases for each plugin whose +names follow the `backstage_plugin_` pattern, e.g. +`backstage_plugin_auth`. You can configure a different database name prefix for +use cases where you have multiple deployments running on a shared database +instance or cluster. -The database manager also allows you to change the database name prefix which is -used when a plugin database isn't explicitly configured. +With infrastructure defined as code or data (Terraform, AWS CloudFormation, +etc.), you may have database credentials which lack permissions to create new +databases or you do not have control over the database names. In these +instances, you can set the database name and connection information on a per +plugin basis as mentioned earlier. -There are two additional configuration options for this database manager: +Backstage supports all of these use cases with the `DatabaseManager` provided by +`@backstage/backend-common`. We will now cover how to use and configure +Backstage's databases. -- **`backend.database.prefix`:** is used to override the default - `backstage_plugin_` prefix which is used to generate a database name when it - is not explicitly set for that plugin. -- **`backend.database.plugin.`:** is used to define a `client` and - `connection` block for the plugin matching the `pluginId`, e.g. `catalog` is - the `pluginId` for the catalog plugin and any configuration defined under that - block is specific to that plugin. +## Prerequisites -## Install Database Drivers +### Dependencies -If you intend to use both `postgres` and `sqlite3`, you need to make sure the -appropriate database drivers are installed in your `backend` package. +Please ensure the appropriate database drivers are installed in your `backend` +package. If you intend to use both `postgres` and `sqlite3`, you can install +both of them. ```shell cd packages/backend @@ -51,48 +49,17 @@ yarn add sqlite3 From an operational perspective, you only need to install drivers for clients that are actively used. -## Add Configuration +### Database Manager -You can set the same type of values for `backend.database..client` and -`backend.database..connection` which are also accepted at the top -level. - -It is possible to override the default database name prefix, -`backstage_plugin_`, which is used when a name isn't explicitly defined. Set -`backend.database.prefix` as shown below. The database names for plugins such as -`catalog` and `auth` would now be `my_company_catalog` and `my_company_auth` -instead of `backstage_plugin_catalog` and `backstage_plugin_auth`. - -```yaml -backend: - database: - client: pg - prefix: my_company_ - connection: - host: localhost - user: postgres - password: password - plugin: - code-coverage: - connection: - database: pg_code_coverage_set_by_user -``` - -In the example above, the `code-coverage` plugin will use the same connection -configuration defined under `database.connection` and use -`pg_code_coverage_set_by_user` instead of `my_company_code-coverage` which would -be automatically generated if a plugin configuration wasn't explicitly set. - -## Integrate `PluginConnectionDatabaseManager` into `backend` - -The `SingleConnectionDatabaseManager` used by default should be replaced with -the `PluginConnectionDatabaseManager` in your `packages/backend/src/index.ts` -file. Import the manager and replace the `.fromConfig` call as shown below: +Existing Backstage instances should be updated to use `DatabaseManager` from +`@backstage/backend-common` in your `packages/backend/src/index.ts` file, the +`SingleConnectionDatabaseManager` has been deprecated. Import the manager and +update the references as shown below if this is not the case: ```diff import { - SingleConnectionDatabaseManager, -+ PluginConnectionDatabaseManager, ++ DatabaseManager, } from '@backstage/backend-common'; // ... @@ -100,24 +67,121 @@ import { function makeCreateEnv(config: Config) { // ... - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); -+ const databaseManager = PluginConnectionDatabaseManager.fromConfig(config); ++ const databaseManager = DatabaseManager.fromConfig(config); // ... } ``` +## Configuration + +You should set the base database client and connection information in your +`app-config.yaml` (or equivalent) file. The base client and configuration is +used as the default which is extended for each plugin with the same or unset +client type. If a client type is specified for a specific plugin which does not +match the base client, the configuration set for the plugin will be used as is +without extending the base configuration. + +Client type and configuration for plugins need to be defined under +**`backend.database.plugin.`**. As an example, `catalog` is the +`pluginId` for the catalog plugin and any configuration defined under that block +is specific to that plugin. We will now explore more detailed example +configurations below. + +### Minimal In-Memory Configuration + +In the example below, we are using `sqlite3` in-memory databases for all +plugins. You may want to use this configuration for testing or other non-durable +use cases. + +```yaml +backend: + database: + client: sqlite3 + connection: ':memory:' +``` + +### PostgreSQL + +The example below uses PostgreSQL (`pg`) as the database client for all plugins. +The `auth` plugin uses a user defined database name instead of the automatically +generated one which would have been `backstage_plugin_auth`. + +```yaml +backend: + database: + client: pg + connection: + host: some.example-pg-instance.tld + user: postgres + password: password + port: 5432 + plugin: + auth: + connection: + database: pg_auth_set_by_user +``` + +### Custom Database Name Prefix + +The configuration below uses `example_prefix_` as the database name prefix +instead of `backstage_plugin_`. Plugins such as `auth` and `catalog` will use +databases named `example_prefix_auth` and `example_prefix_catalog` respectively. + +```yaml +backend: + database: + client: pg + connection: + host: some.example-pg-instance.tld + user: postgres + password: password + port: 5432 + prefix: 'example_prefix_' +``` + +### Connection Configuration Per Plugin + +Both `auth` and `catalog` use connection configuration with different +credentials and database names. This type of configuration can be useful for +environments with infrastructure as code or data which may provide randomly +generated credentials and/or database names. + +```yaml +backend: + database: + client: pg + connection: 'postgresql://some.example-pg-instance.tld:5432' + plugin: + auth: + connection: 'postgresql://fort:knox@some.example-pg-instance.tld:5432/unwitting_fox_jumps' + catalog: + connection: 'postgresql://bank:reserve@some.example-pg-instance.tld:5432/shuffle_ransack_playback' +``` + +### PostgreSQL and SQLite 3 + +The example below uses PostgreSQL (`pg`) as the database client for all plugins +except the `auth` plugin which uses `sqlite3`. As the `auth` plugin's client +type is different from the base client type, the connection configuration for +`auth` is used verbatim without extending the base configuration for PostgreSQL. + +```yaml +backend: + database: + client: pg + connection: 'postgresql://foo:bar@some.example-pg-instance.tld:5432' + plugin: + auth: + client: sqlite3 + connection: ':memory:' +``` + ## Check Your Databases -The `PluginConnectionDatabaseManager` preserves the behaviour of the -`SingleConnectionDatabaseManager`. If the database does not exist, it will -attempt to create it. +The `DatabaseManager` will attempt to create the databases if they do not exist. +If you have set credentials per plugin because the credentials in the base +configuration do not have permissions to create databases, you must ensure they +exist before starting the service. The service will not be able to create them, +it can only use them. -If you are using this database manager to set the database name upfront because -the credentials do not have permissions to create databases, you must ensure -they exist before starting the service. The service will not be able to create -them, it can only use them. - -`sqlite3` databases do not need to be created upfront as with the existing -database manager. - -Your Backstage App can now use different database clients and configuration per -plugin! +Good luck! diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 3ada25dd8a..b42ce3eca5 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -14,16 +14,6 @@ * limitations under the License. */ -export type PluginDatabaseConfig = { - /** Database client to use. */ - client?: 'sqlite3' | 'pg'; - /** - * Database connection to use. - * @secret - */ - connection?: string | object; -}; - export interface Config { app: { baseUrl: string; // defined in core, but repeated here without doc diff --git a/packages/backend-test-utils/src/database/TestDatabases.test.ts b/packages/backend-test-utils/src/database/TestDatabases.test.ts index 7c1e6e1bdd..7a51111b02 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.test.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.test.ts @@ -71,7 +71,7 @@ describe('TestDatabases', () => { await input.insert({ x: 'y' }).into('a'); // Look for the mark - const database = 'backstage_plugin_0'; + const database = 'backstage_plugin_db0'; const output = knexFactory({ client: 'pg', connection: { host, port, user, password, database }, @@ -105,7 +105,7 @@ describe('TestDatabases', () => { await input.insert({ x: 'y' }).into('a'); // Look for the mark - const database = 'backstage_plugin_0'; + const database = 'backstage_plugin_db0'; const output = knexFactory({ client: 'pg', connection: { host, port, user, password, database }, @@ -139,7 +139,7 @@ describe('TestDatabases', () => { await input.insert({ x: 'y' }).into('a'); // Look for the mark - const database = 'backstage_plugin_0'; + const database = 'backstage_plugin_db0'; const output = knexFactory({ client: 'mysql2', connection: { host, port, user, password, database }, diff --git a/packages/backend-test-utils/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts index ab5846d84d..0ba6bffc0b 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SingleConnectionDatabaseManager } from '@backstage/backend-common'; +import { DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { Knex } from 'knex'; import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; @@ -142,7 +142,7 @@ export class TestDatabases { // Ensure that a unique logical database is created in the instance const connection = await instance.databaseManager - .forPlugin(String(this.lastDatabaseIndex++)) + .forPlugin(String(`db${this.lastDatabaseIndex++}`)) .getClient(); instance.connections.push(connection); @@ -157,7 +157,7 @@ export class TestDatabases { if (envVarName) { const connectionString = process.env[envVarName]; if (connectionString) { - const databaseManager = SingleConnectionDatabaseManager.fromConfig( + const databaseManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -195,7 +195,7 @@ export class TestDatabases { properties.dockerImageName!, ); - const databaseManager = SingleConnectionDatabaseManager.fromConfig( + const databaseManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -220,7 +220,7 @@ export class TestDatabases { properties.dockerImageName!, ); - const databaseManager = SingleConnectionDatabaseManager.fromConfig( + const databaseManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -241,7 +241,7 @@ export class TestDatabases { private async initSqlite( _properties: TestDatabaseProperties, ): Promise { - const databaseManager = SingleConnectionDatabaseManager.fromConfig( + const databaseManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts index 791cf09b7b..91b5939765 100644 --- a/packages/backend-test-utils/src/database/types.ts +++ b/packages/backend-test-utils/src/database/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SingleConnectionDatabaseManager } from '@backstage/backend-common'; +import { DatabaseManager } from '@backstage/backend-common'; import { Knex } from 'knex'; /** @@ -35,10 +35,9 @@ export type TestDatabaseProperties = { export type Instance = { stopContainer?: () => Promise; - databaseManager: SingleConnectionDatabaseManager; + databaseManager: DatabaseManager; connections: Array; }; - export const allDatabases: Record< TestDatabaseId, TestDatabaseProperties diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index aebd034aae..70f4fb676e 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -14,7 +14,7 @@ import { useHotMemoize, notFoundHandler, CacheManager, - SingleConnectionDatabaseManager, + DatabaseManager, SingleHostDiscovery, UrlReaders, } from '@backstage/backend-common'; @@ -34,8 +34,8 @@ function makeCreateEnv(config: Config) { root.info(`Created UrlReader ${reader}`); - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); const cacheManager = CacheManager.fromConfig(config); + const databaseManager = DatabaseManager.fromConfig(config); return (plugin: string): PluginEnvironment => { const logger = root.child({ type: 'plugin', plugin }); diff --git a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts index 688eeb4ba2..fec2ea7afa 100644 --- a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SingleConnectionDatabaseManager } from '@backstage/backend-common'; +import { DatabaseManager } from '@backstage/backend-common'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { @@ -22,7 +22,7 @@ import { } from './CodeCoverageDatabase'; import { JsonCodeCoverage } from './types'; -const db = SingleConnectionDatabaseManager.fromConfig( +const db = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { diff --git a/plugins/code-coverage-backend/src/service/router.test.ts b/plugins/code-coverage-backend/src/service/router.test.ts index e18cd75c2f..340502d1e6 100644 --- a/plugins/code-coverage-backend/src/service/router.test.ts +++ b/plugins/code-coverage-backend/src/service/router.test.ts @@ -20,14 +20,14 @@ import { getVoidLogger, PluginDatabaseManager, PluginEndpointDiscovery, - SingleConnectionDatabaseManager, + DatabaseManager, UrlReaders, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { createRouter } from './router'; function createDatabase(): PluginDatabaseManager { - return SingleConnectionDatabaseManager.fromConfig( + return DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 686dfc052f..5ed997bb25 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -14,17 +14,14 @@ * limitations under the License. */ -import { - getVoidLogger, - SingleConnectionDatabaseManager, -} from '@backstage/backend-common'; +import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; import { TaskSecrets, TaskSpec, DbTaskEventRow } from './types'; async function createStore(): Promise { - const manager = SingleConnectionDatabaseManager.fromConfig( + const manager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index c6ad1a8105..f8343e7d3c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -14,12 +14,9 @@ * limitations under the License. */ -import { - getVoidLogger, - SingleConnectionDatabaseManager, -} from '@backstage/backend-common'; -import { ConfigReader, JsonObject } from '@backstage/config'; import os from 'os'; +import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; +import { ConfigReader, JsonObject } from '@backstage/config'; import { createTemplateAction, TemplateActionRegistry } from '../actions'; import { RepoSpec } from '../actions/builtin/publish/util'; import { DatabaseTaskStore } from './DatabaseTaskStore'; @@ -27,7 +24,7 @@ import { StorageTaskBroker } from './StorageTaskBroker'; import { TaskWorker } from './TaskWorker'; async function createStore(): Promise { - const manager = SingleConnectionDatabaseManager.fromConfig( + const manager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 9ca2eef9e6..9349e076fd 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -31,7 +31,7 @@ jest.doMock('fs-extra', () => ({ import { getVoidLogger, PluginDatabaseManager, - SingleConnectionDatabaseManager, + DatabaseManager, UrlReaders, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; @@ -47,7 +47,7 @@ const createCatalogClient = (templates: any[] = []) => } as CatalogApi); function createDatabase(): PluginDatabaseManager { - return SingleConnectionDatabaseManager.fromConfig( + return DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { From dc75c78cd776709b80ca82ce6924b449c5aabeca Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Sat, 5 Jun 2021 23:15:44 +0100 Subject: [PATCH 028/206] refactor: split up database manager private methods Signed-off-by: Minn Soe --- packages/backend-common/api-report.md | 4 +- .../src/database/DatabaseManager.ts | 169 ++++++++++++------ .../backend-common/src/database/connection.ts | 4 +- 3 files changed, 115 insertions(+), 62 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index d4d075004a..ed78157810 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -75,7 +75,7 @@ export interface CacheClient { export class CacheManager { forPlugin(pluginId: string): PluginCacheManager; static fromConfig(config: Config, options?: CacheManagerOptions): CacheManager; -} + } // @public (undocumented) export const coloredFormat: winston.Logform.Format; @@ -260,7 +260,7 @@ export class GitlabUrlReader implements UrlReader { export function loadBackendConfig(options: Options): Promise; // @public -export function normalizeConnection(connection: Knex.StaticConnectionConfig | JsonObject | string, client: string): Record; +export function normalizeConnection(connection: Knex.StaticConnectionConfig | JsonObject | string | undefined, client: string): Partial; // @public export function notFoundHandler(): RequestHandler; diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 34ceea55e3..7513422452 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -15,7 +15,7 @@ */ import { Knex } from 'knex'; import { omit } from 'lodash'; -import { Config, ConfigReader } from '@backstage/config'; +import { Config, ConfigReader, JsonObject } from '@backstage/config'; import { createDatabaseClient, ensureDatabaseExists, @@ -24,6 +24,9 @@ import { } from './connection'; import { PluginDatabaseManager } from './types'; +/** + * Provides a config lookup path for a plugin's config block. + */ function pluginPath(pluginId: string): string { return `plugin.${pluginId}`; } @@ -56,8 +59,9 @@ export class DatabaseManager { /** * Generates a PluginDatabaseManager for consumption by plugins. * - * @param pluginId The plugin that the database manager should be created for. Plugin names should be unique - * as they are used to look up database config overrides under `backend.database.plugin`. + * @param pluginId The plugin that the database manager should be created for. Plugin names + * should be unique as they are used to look up database config overrides under + * `backend.database.plugin`. */ forPlugin(pluginId: string): PluginDatabaseManager { const _this = this; @@ -70,7 +74,7 @@ export class DatabaseManager { } /** - * Provides the canonical database name for a given pluginId. + * Provides the canonical database name for a given plugin. * * This method provides the effective database name which is determined using global * and plugin specific database config. If no explicit database name is configured, @@ -78,83 +82,132 @@ export class DatabaseManager { * 'backstage_plugin_'. * * @param pluginId Lookup the database name for given plugin + * @returns String representing the plugin's database name */ private getDatabaseName(pluginId: string): string { - const pluginConfig: Config = this.getConfigForPlugin(pluginId); + const connection = this.getConnectionConfig(pluginId); - // determine root sqlite config to pass through as this is a special case - const rootConnection = this.config.get('connection'); - const rootSqliteName = - typeof rootConnection === 'string' - ? rootConnection - : this.config.getOptionalString('connection.filename') ?? ':memory:'; - - const isSqlite = this.config.getString('client') === 'sqlite3'; + if (this.getClientType(pluginId).client === 'sqlite3') { + // sqlite database name should fallback to ':memory:' as a special case + return ( + (connection as Knex.Sqlite3ConnectionConfig)?.filename ?? ':memory:' + ); + } + // all other supported databases should fallback to an auto-prefixed name return ( - // attempt to lookup pg and mysql database name - pluginConfig.getOptionalString('connection.database') ?? - // attempt to lookup sqlite3 database file name - pluginConfig.getOptionalString('connection.filename') ?? - // if root is sqlite - attempt to use top level connection, fallback to :memory: - (isSqlite ? rootSqliteName : null) ?? - // generate a database name using prefix and pluginId + (connection as Knex.ConnectionConfig)?.database ?? `${this.prefix}${pluginId}` ); } /** - * Provides a base database connector config by merging different config sources. + * Provides the client type which should be used for a given plugin. * - * This method provides a baseConfig for a database connector without the target - * database's name property ('database', 'filename'). The client type is determined - * by plugin specific config which uses the default as the fallback. + * The client type is determined by plugin specific config if present. Otherwise the base + * client is used as the fallback. * - * If the client type is the same as the plugin or not specified, the global - * connection config will be extended with plugin specific config. - * - * @param pluginId The plugin that the database baseConfig should correspond to + * @param pluginId Plugin to get the client type for + * @returns Object with client type returned as `client` and boolean representing whether + * or not the client was overridden as `overridden` */ - private getConfigForPlugin(pluginId: string): Config { - const pluginConfig = this.config.getOptionalConfig(pluginPath(pluginId)); + private getClientType( + pluginId: string, + ): { + client: string; + overridden: boolean; + } { + const pluginClient = this.config.getOptionalString( + `${pluginPath(pluginId)}.client`, + ); const baseClient = this.config.getString('client'); - const client = pluginConfig?.getOptionalString('client') ?? baseClient; - - const baseConnection = normalizeConnection( - this.config.get('connection'), - baseClient, - ); - const connection = normalizeConnection( - pluginConfig?.getOptional('connection') ?? {}, + const client = pluginClient ?? baseClient; + return { client, - ); - - return new ConfigReader({ - client, - connection: { - // if same client type, extend original connection config without dbname config - ...(client === baseClient - ? omit(baseConnection, ['database', 'filename']) - : {}), - ...connection, - }, - }); + overridden: client !== baseClient, + }; } + /** + * Provides a Knex connection plugin config by combining base and plugin config. + * + * This method provides a baseConfig for a plugin database connector. If the client type + * has not been overridden, the global connection config will be included with plugin + * specific config as the base. Values from the plugin connection take precedence over the + * base. Base database name is omitted for all supported databases excluding SQLite. + */ + private getConnectionConfig( + pluginId: string, + ): Partial { + const { client, overridden } = this.getClientType(pluginId); + + let baseConnection = normalizeConnection( + this.config.get('connection'), + this.config.getString('client'), + ); + // As databases cannot be shared, the `database` property from the base connection + // is omitted. SQLite3's `filename` property is an exception as this is used as a + // directory elsewhere so we preserve `filename`. + baseConnection = omit(baseConnection, 'database'); + + // get and normalize optional plugin specific database connection + const connection = normalizeConnection( + this.config.getOptional(`${pluginPath(pluginId)}.connection`), + client, + ); + + return { + // include base connection if client type has not been overriden + ...(overridden ? {} : baseConnection), + ...connection, + }; + } + + /** + * Provides a Knex database config for a given plugin. + * + * This method provides a Knex configuration object along with the plugin's client type. + * + * @param pluginId The plugin that the database config should correspond with + */ + private getConfigForPlugin(pluginId: string): Knex.Config { + const { client } = this.getClientType(pluginId); + + return { + client, + connection: this.getConnectionConfig(pluginId), + }; + } + + /** + * Provides a partial Knex.Config database name override for a given plugin. + * + * @param pluginId Target plugin to get database name override + * @returns Partial Knex.Config with database name override + */ + private getDatabaseOverrides(pluginId: string): Knex.Config { + return createNameOverride( + this.getClientType(pluginId).client, + this.getDatabaseName(pluginId), + ); + } + + /** + * Provides a scoped Knex client for a plugin as per application config. + * + * @param pluginId Plugin to get a Knex client for + * @returns Promise which resolves to a scoped Knex database client for a plugin + */ private async getDatabase(pluginId: string): Promise { - const pluginConfig = this.getConfigForPlugin(pluginId); + const pluginConfig = new ConfigReader( + this.getConfigForPlugin(pluginId) as JsonObject, + ); await ensureDatabaseExists(pluginConfig, this.getDatabaseName(pluginId)); + return createDatabaseClient( pluginConfig, this.getDatabaseOverrides(pluginId), ); } - - private getDatabaseOverrides(pluginId: string): Knex.Config { - return createNameOverride( - this.getConfigForPlugin(pluginId).get('client'), - this.getDatabaseName(pluginId), - ); - } } diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 005ed8819f..2f6d204839 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -121,9 +121,9 @@ export function parseConnectionString( * Normalizes a connection config or string into an object which can be passed to Knex. */ export function normalizeConnection( - connection: Knex.StaticConnectionConfig | JsonObject | string, + connection: Knex.StaticConnectionConfig | JsonObject | string | undefined, client: string, -): Record { +): Partial { if (typeof connection === 'undefined' || connection === null) { return {}; } From 214efffe3ac2f99dd08c37c3130fe4742511d271 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Sun, 6 Jun 2021 16:56:38 +0100 Subject: [PATCH 029/206] refactor: move database connector interface Signed-off-by: Minn Soe --- .../backend-common/src/database/connection.ts | 2 +- .../backend-common/src/database/connector.ts | 30 ---------------- packages/backend-common/src/database/mysql.ts | 2 +- .../backend-common/src/database/postgres.ts | 2 +- .../backend-common/src/database/sqlite3.ts | 2 +- packages/backend-common/src/database/types.ts | 35 +++++++++++++++++++ 6 files changed, 39 insertions(+), 34 deletions(-) delete mode 100644 packages/backend-common/src/database/connector.ts diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 2f6d204839..2c2bba28ec 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -18,7 +18,7 @@ import { Config, JsonObject } from '@backstage/config'; import { InputError } from '@backstage/errors'; import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; -import { DatabaseConnector } from './connector'; +import { DatabaseConnector } from './types'; import { mysqlConnector } from './mysql'; import { pgConnector } from './postgres'; diff --git a/packages/backend-common/src/database/connector.ts b/packages/backend-common/src/database/connector.ts deleted file mode 100644 index 0336510f19..0000000000 --- a/packages/backend-common/src/database/connector.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Config } from '@backstage/config'; -import { Knex } from 'knex'; - -export interface DatabaseConnector { - createClient(dbConfig: Config, overrides?: Partial): Knex; - createNameOverride(name: string): Partial; - parseConnectionString( - connectionString: string, - client?: string, - ): Knex.StaticConnectionConfig; - ensureDatabaseExists?( - dbConfig: Config, - ...databases: Array - ): Promise; -} diff --git a/packages/backend-common/src/database/mysql.ts b/packages/backend-common/src/database/mysql.ts index beff5d9f82..0769ad64fc 100644 --- a/packages/backend-common/src/database/mysql.ts +++ b/packages/backend-common/src/database/mysql.ts @@ -18,7 +18,7 @@ import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; -import { DatabaseConnector } from './connector'; +import { DatabaseConnector } from './types'; import yn from 'yn'; /** diff --git a/packages/backend-common/src/database/postgres.ts b/packages/backend-common/src/database/postgres.ts index 000b84460c..5662028113 100644 --- a/packages/backend-common/src/database/postgres.ts +++ b/packages/backend-common/src/database/postgres.ts @@ -17,7 +17,7 @@ import knexFactory, { Knex } from 'knex'; import { Config } from '@backstage/config'; import { mergeDatabaseConfig } from './config'; -import { DatabaseConnector } from './connector'; +import { DatabaseConnector } from './types'; /** * Creates a knex postgres database connection diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/sqlite3.ts index 83f5f60699..2499415bef 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/sqlite3.ts @@ -19,7 +19,7 @@ import { ensureDirSync } from 'fs-extra'; import knexFactory, { Knex } from 'knex'; import path from 'path'; import { mergeDatabaseConfig } from './config'; -import { DatabaseConnector } from './connector'; +import { DatabaseConnector } from './types'; /** * Creates a knex sqlite3 database connection diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index bf3ceb2786..995f98f27d 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Config } from '@backstage/config'; import { Knex } from 'knex'; /** @@ -28,3 +29,37 @@ export interface PluginDatabaseManager { */ getClient(): Promise; } + +/** + * DatabaseConnector manages an underlying Knex database driver. + */ +export interface DatabaseConnector { + /** + * createClient provides an instance of a knex database connector. + */ + createClient(dbConfig: Config, overrides?: Partial): Knex; + /** + * createNameOverride provides a partial knex config sufficient to override a + * database name. + */ + createNameOverride(name: string): Partial; + /** + * parseConnectionString produces a knex connection config object representing + * a database connection string. + */ + parseConnectionString( + connectionString: string, + client?: string, + ): Knex.StaticConnectionConfig; + /** + * ensureDatabaseExists performs a side-effect to ensure database names passed in are + * present. + * + * Calling this function on databases which already exist should do nothing. + * Missing databases should be created if needed. + */ + ensureDatabaseExists?( + dbConfig: Config, + ...databases: Array + ): Promise; +} From f1a9108c53a9c1ff3cdf9a09e226dd556f6930b8 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Mon, 7 Jun 2021 13:07:24 +0100 Subject: [PATCH 030/206] refactor: add defaultNameOverride and reorganize Signed-off-by: Minn Soe --- packages/backend-common/api-report.md | 8 +++++ .../backend-common/src/database/connection.ts | 4 +-- .../connectors/defaultNameOverride.test.ts | 26 ++++++++++++++ .../connectors/defaultNameOverride.ts | 34 +++++++++++++++++++ .../src/database/connectors/index.ts | 18 ++++++++++ .../database/{ => connectors}/mysql.test.ts | 0 .../src/database/{ => connectors}/mysql.ts | 22 +++++------- .../{ => connectors}/postgres.test.ts | 0 .../src/database/{ => connectors}/postgres.ts | 16 +++------ .../database/{ => connectors}/sqlite3.test.ts | 0 .../src/database/{ => connectors}/sqlite3.ts | 21 ++++++++---- 11 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 packages/backend-common/src/database/connectors/defaultNameOverride.test.ts create mode 100644 packages/backend-common/src/database/connectors/defaultNameOverride.ts create mode 100644 packages/backend-common/src/database/connectors/index.ts rename packages/backend-common/src/database/{ => connectors}/mysql.test.ts (100%) rename packages/backend-common/src/database/{ => connectors}/mysql.ts (93%) rename packages/backend-common/src/database/{ => connectors}/postgres.test.ts (100%) rename packages/backend-common/src/database/{ => connectors}/postgres.ts (93%) rename packages/backend-common/src/database/{ => connectors}/sqlite3.test.ts (100%) rename packages/backend-common/src/database/{ => connectors}/sqlite3.ts (90%) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index ed78157810..1cf1f4f21b 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -104,6 +104,14 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl; // @public (undocumented) export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise; +// @public +export interface DatabaseConnector { + createClient(dbConfig: Config, overrides?: Partial): Knex; + createNameOverride(name: string): Partial; + ensureDatabaseExists?(dbConfig: Config, ...databases: Array): Promise; + parseConnectionString(connectionString: string, client?: string): Knex.StaticConnectionConfig; +} + // @public (undocumented) export class DatabaseManager { forPlugin(pluginId: string): PluginDatabaseManager; diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 2c2bba28ec..46f040d9d0 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -20,9 +20,7 @@ import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; import { DatabaseConnector } from './types'; -import { mysqlConnector } from './mysql'; -import { pgConnector } from './postgres'; -import { sqlite3Connector } from './sqlite3'; +import { mysqlConnector, pgConnector, sqlite3Connector } from './connectors'; type DatabaseClient = 'pg' | 'sqlite3' | 'mysql' | 'mysql2' | string; diff --git a/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts b/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts new file mode 100644 index 0000000000..b41736153a --- /dev/null +++ b/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import defaultNameOverride from './defaultNameOverride'; + +describe('defaultNameOverride()', () => { + it('returns a partial knex static connection config with database name', () => { + const testDatabaseName = 'testdatabase'; + expect(defaultNameOverride(testDatabaseName)).toHaveProperty( + 'connection.database', + testDatabaseName, + ); + }); +}); diff --git a/packages/backend-common/src/database/connectors/defaultNameOverride.ts b/packages/backend-common/src/database/connectors/defaultNameOverride.ts new file mode 100644 index 0000000000..6296010c76 --- /dev/null +++ b/packages/backend-common/src/database/connectors/defaultNameOverride.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Knex } from 'knex'; + +/** + * Provides a partial knex config with database name override. + * + * Default override for knex database drivers which accept ConnectionConfig + * with `connection.database` as the database name field. + * + * @param name database name to get config override for + */ +export default function defaultNameOverride( + name: string, +): Partial { + return { + connection: { + database: name, + }, + }; +} diff --git a/packages/backend-common/src/database/connectors/index.ts b/packages/backend-common/src/database/connectors/index.ts new file mode 100644 index 0000000000..f314bb5004 --- /dev/null +++ b/packages/backend-common/src/database/connectors/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './mysql'; +export * from './postgres'; +export * from './sqlite3'; diff --git a/packages/backend-common/src/database/mysql.test.ts b/packages/backend-common/src/database/connectors/mysql.test.ts similarity index 100% rename from packages/backend-common/src/database/mysql.test.ts rename to packages/backend-common/src/database/connectors/mysql.test.ts diff --git a/packages/backend-common/src/database/mysql.ts b/packages/backend-common/src/database/connectors/mysql.ts similarity index 93% rename from packages/backend-common/src/database/mysql.ts rename to packages/backend-common/src/database/connectors/mysql.ts index 0769ad64fc..60f1e09ce0 100644 --- a/packages/backend-common/src/database/mysql.ts +++ b/packages/backend-common/src/database/connectors/mysql.ts @@ -14,12 +14,14 @@ * limitations under the License. */ +import knexFactory, { Knex } from 'knex'; +import yn from 'yn'; + import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; -import knexFactory, { Knex } from 'knex'; -import { mergeDatabaseConfig } from './config'; -import { DatabaseConnector } from './types'; -import yn from 'yn'; +import { mergeDatabaseConfig } from '../config'; +import { DatabaseConnector } from '../types'; +import defaultNameOverride from './defaultNameOverride'; /** * Creates a knex mysql database connection @@ -161,22 +163,14 @@ export async function ensureMysqlDatabaseExists( } } -export function createMysqlNameOverride(name: string): Partial { - return { - connection: { - database: name, - }, - }; -} - /** - * MySql database connector. + * MySQL database connector. * * Exposes database connector functionality via an immutable object. */ export const mysqlConnector: DatabaseConnector = Object.freeze({ createClient: createMysqlDatabaseClient, ensureDatabaseExists: ensureMysqlDatabaseExists, - createNameOverride: createMysqlNameOverride, + createNameOverride: defaultNameOverride, parseConnectionString: parseMysqlConnectionString, }); diff --git a/packages/backend-common/src/database/postgres.test.ts b/packages/backend-common/src/database/connectors/postgres.test.ts similarity index 100% rename from packages/backend-common/src/database/postgres.test.ts rename to packages/backend-common/src/database/connectors/postgres.test.ts diff --git a/packages/backend-common/src/database/postgres.ts b/packages/backend-common/src/database/connectors/postgres.ts similarity index 93% rename from packages/backend-common/src/database/postgres.ts rename to packages/backend-common/src/database/connectors/postgres.ts index 5662028113..011e40579b 100644 --- a/packages/backend-common/src/database/postgres.ts +++ b/packages/backend-common/src/database/connectors/postgres.ts @@ -15,9 +15,11 @@ */ import knexFactory, { Knex } from 'knex'; + import { Config } from '@backstage/config'; -import { mergeDatabaseConfig } from './config'; -import { DatabaseConnector } from './types'; +import { mergeDatabaseConfig } from '../config'; +import { DatabaseConnector } from '../types'; +import defaultNameOverride from './defaultNameOverride'; /** * Creates a knex postgres database connection @@ -133,14 +135,6 @@ export async function ensurePgDatabaseExists( } } -export function createPgNameOverride(name: string): Partial { - return { - connection: { - database: name, - }, - }; -} - /** * PostgreSQL database connector. * @@ -149,6 +143,6 @@ export function createPgNameOverride(name: string): Partial { export const pgConnector: DatabaseConnector = Object.freeze({ createClient: createPgDatabaseClient, ensureDatabaseExists: ensurePgDatabaseExists, - createNameOverride: createPgNameOverride, + createNameOverride: defaultNameOverride, parseConnectionString: parsePgConnectionString, }); diff --git a/packages/backend-common/src/database/sqlite3.test.ts b/packages/backend-common/src/database/connectors/sqlite3.test.ts similarity index 100% rename from packages/backend-common/src/database/sqlite3.test.ts rename to packages/backend-common/src/database/connectors/sqlite3.test.ts diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts similarity index 90% rename from packages/backend-common/src/database/sqlite3.ts rename to packages/backend-common/src/database/connectors/sqlite3.ts index 2499415bef..c9e86c80da 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import path from 'path'; -import { Config } from '@backstage/config'; import { ensureDirSync } from 'fs-extra'; import knexFactory, { Knex } from 'knex'; -import path from 'path'; -import { mergeDatabaseConfig } from './config'; -import { DatabaseConnector } from './types'; + +import { Config } from '@backstage/config'; +import { mergeDatabaseConfig } from '../config'; +import { DatabaseConnector } from '../types'; /** - * Creates a knex sqlite3 database connection + * Creates a knex SQLite3 database connection * * @param dbConfig The database config * @param overrides Additional options to merge with the config @@ -55,7 +56,7 @@ export function createSqliteDatabaseClient( } /** - * Builds a knex sqlite3 connection config + * Builds a knex SQLite3 connection config * * @param dbConfig The database config * @param overrides Additional options to merge with the config @@ -101,12 +102,18 @@ export function buildSqliteDatabaseConfig( return config; } +/** + * Provides a partial knex SQLite3 config to override database name. + */ export function createSqliteNameOverride(name: string): Partial { return { connection: parseSqliteConnectionString(name), }; } +/** + * Produces a partial knex SQLite3 connection config with database name. + */ export function parseSqliteConnectionString( name: string, ): Knex.Sqlite3ConnectionConfig { @@ -116,7 +123,7 @@ export function parseSqliteConnectionString( } /** - * Sqlite3 database connector. + * SQLite3 database connector. * * Exposes database connector functionality via an immutable object. */ From 946db4caf65d4693cc4ed11f740b123095284a4e Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Thu, 10 Jun 2021 11:11:21 +0100 Subject: [PATCH 031/206] docs: expand changeset scope `@backstage/backend-test-utils` has been updated to use a plugin name which starts with an alphabetical character, changeset now includes this package. Signed-off-by: Minn Soe --- .changeset/five-donkeys-brake.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md index 8a6523bb61..b0bb635876 100644 --- a/.changeset/five-donkeys-brake.md +++ b/.changeset/five-donkeys-brake.md @@ -1,6 +1,7 @@ --- '@backstage/backend-common': minor '@backstage/create-app': minor +'@backstage/backend-test-utils': minor --- Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database From 5db7445b4557e5a18ea9309d7c4addc0c52446b3 Mon Sep 17 00:00:00 2001 From: Daniel Ortega Date: Thu, 10 Jun 2021 13:54:12 +0200 Subject: [PATCH 032/206] #5986 Adding required changeset Signed-off-by: Daniel Ortega --- .changeset/clean-frogs-brake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-frogs-brake.md diff --git a/.changeset/clean-frogs-brake.md b/.changeset/clean-frogs-brake.md new file mode 100644 index 0000000000..a6d7dd0c08 --- /dev/null +++ b/.changeset/clean-frogs-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +Adding .DS_Store pattern to .gitignore in Scaffolded Backstage App. To migrate an existing app that pattern should be added manually. From d2c31b132ed67efd883f2217ac3e1a36f02ee89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 10 Jun 2021 11:00:34 +0200 Subject: [PATCH 033/206] Add Title prop in SupportButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Victor Perera Signed-off-by: Fredrik Adelöw --- .changeset/tall-bears-taste.md | 5 ++++ .../SupportButton/SupportButton.test.tsx | 9 ++++++- .../SupportButton/SupportButton.tsx | 26 ++++++++++++------- 3 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 .changeset/tall-bears-taste.md diff --git a/.changeset/tall-bears-taste.md b/.changeset/tall-bears-taste.md new file mode 100644 index 0000000000..050c47867c --- /dev/null +++ b/.changeset/tall-bears-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add title prop in SupportButton component diff --git a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx index 6d6b9fd477..7c6cab99c1 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx @@ -21,8 +21,9 @@ import { RenderResult, waitFor, fireEvent, + screen, } from '@testing-library/react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { wrapInTestApp, renderInTestApp } from '@backstage/test-utils'; import { SupportButton } from './SupportButton'; const SUPPORT_BUTTON_ID = 'support-button'; @@ -41,6 +42,12 @@ describe('', () => { ); }); + it('supports passing a title', async () => { + await renderInTestApp(); + fireEvent.click(screen.getByTestId(SUPPORT_BUTTON_ID)); + expect(screen.getByText('Custom title')).toBeInTheDocument(); + }); + it('shows popover on click', async () => { let renderResult: RenderResult; diff --git a/packages/core-components/src/components/SupportButton/SupportButton.tsx b/packages/core-components/src/components/SupportButton/SupportButton.tsx index 6c6de8abe3..96cc308f74 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.tsx @@ -24,19 +24,18 @@ import { ListItem, ListItemIcon, ListItemText, + Typography, makeStyles, Popover, } from '@material-ui/core'; -import React, { - Fragment, - MouseEventHandler, - PropsWithChildren, - useState, -} from 'react'; +import React, { Fragment, MouseEventHandler, useState } from 'react'; import { SupportItem, SupportItemLink, useSupportConfig } from '../../hooks'; import { Link } from '../Link'; -type Props = {}; +type SupportButtonProps = { + title?: string; + children?: React.ReactNode; +}; const useStyles = makeStyles({ popoverList: { @@ -78,7 +77,7 @@ const SupportListItem = ({ item }: { item: SupportItem }) => { ); }; -export const SupportButton = ({ children }: PropsWithChildren) => { +export const SupportButton = ({ title, children }: SupportButtonProps) => { const { items } = useSupportConfig(); const [popoverOpen, setPopoverOpen] = useState(false); @@ -121,13 +120,20 @@ export const SupportButton = ({ children }: PropsWithChildren) => { onClose={popoverCloseHandler} > + {title && ( + + {title} + + )} {React.Children.map(children, (child, i) => ( - + {child} ))} {items && - items.map((item, i) => )} + items.map((item, i) => ( + + ))} - )} + All your APIs - +
+ +
+
+ +
+
); diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx deleted file mode 100644 index bcb310be46..0000000000 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx +++ /dev/null @@ -1,81 +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 { Entity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; -import * as React from 'react'; -import { apiDocsConfigRef } from '../../config'; -import { ApiExplorerTable } from './ApiExplorerTable'; - -const entities: Entity[] = [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'API', - metadata: { name: 'api1' }, - spec: { type: 'openapi' }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'API', - metadata: { name: 'api2' }, - spec: { type: 'openapi' }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'API', - metadata: { name: 'api3' }, - spec: { type: 'grpc' }, - }, -]; - -const apiRegistry = ApiRegistry.with(apiDocsConfigRef, { - getApiDefinitionWidget: () => undefined, -}); - -describe('ApiCatalogTable component', () => { - it('should render error message when error is passed in props', async () => { - const rendered = render( - wrapInTestApp( - - - , - ), - ); - const errorMessage = await rendered.findByText( - /Could not fetch catalog entities./, - ); - expect(errorMessage).toBeInTheDocument(); - }); - - it('should display entity names when loading has finished and no error occurred', async () => { - const rendered = render( - wrapInTestApp( - - - , - ), - ); - expect(rendered.getByText(/api1/)).toBeInTheDocument(); - expect(rendered.getByText(/api2/)).toBeInTheDocument(); - expect(rendered.getByText(/api3/)).toBeInTheDocument(); - }); -}); diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx deleted file mode 100644 index 78b5f1d240..0000000000 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ /dev/null @@ -1,216 +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 { - ApiEntityV1alpha1, - Entity, - EntityName, - RELATION_OWNED_BY, - RELATION_PART_OF, -} from '@backstage/catalog-model'; -import { - CodeSnippet, - OverflowTooltip, - Table, - TableColumn, - TableFilter, - TableState, - useQueryParamState, - WarningPanel, -} from '@backstage/core'; -import { - EntityRefLink, - EntityRefLinks, - formatEntityRefTitle, - getEntityRelations, -} from '@backstage/plugin-catalog-react'; -import { Chip } from '@material-ui/core'; -import React from 'react'; -import { ApiTypeTitle } from '../ApiDefinitionCard'; - -type EntityRow = { - entity: ApiEntityV1alpha1; - resolved: { - name: string; - partOfSystemRelationTitle?: string; - partOfSystemRelations: EntityName[]; - ownedByRelationsTitle?: string; - ownedByRelations: EntityName[]; - }; -}; - -const columns: TableColumn[] = [ - { - title: 'Name', - field: 'resolved.name', - highlight: true, - render: ({ entity }) => ( - - ), - }, - { - title: 'System', - field: 'resolved.partOfSystemRelationTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Owner', - field: 'resolved.ownedByRelationsTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Lifecycle', - field: 'entity.spec.lifecycle', - }, - { - title: 'Type', - field: 'entity.spec.type', - render: ({ entity }) => , - }, - { - title: 'Description', - field: 'entity.metadata.description', - render: ({ entity }) => ( - - ), - width: 'auto', - }, - { - title: 'Tags', - field: 'entity.metadata.tags', - cellStyle: { - padding: '0px 16px 0px 20px', - }, - render: ({ entity }) => ( - <> - {entity.metadata.tags && - entity.metadata.tags.map(t => ( - - ))} - - ), - }, -]; - -const filters: TableFilter[] = [ - { - column: 'Owner', - type: 'select', - }, - { - column: 'Type', - type: 'multiple-select', - }, - { - column: 'Lifecycle', - type: 'multiple-select', - }, - { - column: 'Tags', - type: 'checkbox-tree', - }, -]; - -type ExplorerTableProps = { - entities: Entity[]; - loading: boolean; - error?: any; -}; - -export const ApiExplorerTable = ({ - entities, - loading, - error, -}: ExplorerTableProps) => { - const [queryParamState, setQueryParamState] = useQueryParamState( - 'apiTable', - ); - - if (error) { - return ( - - - - ); - } - - const rows = entities.map(entity => { - const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, { - kind: 'system', - }); - const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); - - return { - entity: entity as ApiEntityV1alpha1, - resolved: { - name: formatEntityRefTitle(entity, { - defaultKind: 'API', - }), - ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) - .join(', '), - ownedByRelations, - partOfSystemRelationTitle: partOfSystemRelations - .map(r => - formatEntityRefTitle(r, { - defaultKind: 'system', - }), - ) - .join(', '), - partOfSystemRelations, - }, - }; - }); - - return ( - - isLoading={loading} - columns={columns} - options={{ - paging: true, - pageSize: 20, - pageSizeOptions: [20, 50, 100], - actionsColumnIndex: -1, - loadingType: 'linear', - padding: 'dense', - showEmptyDataSourceMessage: !loading, - }} - data={rows} - filters={filters} - initialState={queryParamState} - onStateChange={setQueryParamState} - /> - ); -}; diff --git a/plugins/api-docs/src/components/ApiExplorerTable/index.ts b/plugins/api-docs/src/components/ApiExplorerTable/index.ts deleted file mode 100644 index a9c79861e8..0000000000 --- a/plugins/api-docs/src/components/ApiExplorerTable/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 { ApiExplorerTable } from './ApiExplorerTable'; diff --git a/plugins/catalog/src/components/CatalogTable/index.ts b/plugins/catalog/src/components/CatalogTable/index.ts index 280d5b4bcb..460720245e 100644 --- a/plugins/catalog/src/components/CatalogTable/index.ts +++ b/plugins/catalog/src/components/CatalogTable/index.ts @@ -15,3 +15,4 @@ */ export { CatalogTable } from './CatalogTable'; +export type { EntityRow } from './types'; diff --git a/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx b/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx index 896e9bada3..8c30c9f9be 100644 --- a/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx +++ b/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx @@ -20,7 +20,12 @@ import { Button } from '@material-ui/core'; import { useRouteRef } from '@backstage/core'; import { createComponentRouteRef } from '../../routes'; -export const CreateComponentButton = () => { +type CreateComponentButtonProps = { + buttonLabel?: string; +}; +export const CreateComponentButton = ({ + buttonLabel, +}: CreateComponentButtonProps) => { const createComponentLink = useRouteRef(createComponentRouteRef); if (!createComponentLink) return null; @@ -32,7 +37,7 @@ export const CreateComponentButton = () => { color="primary" to={createComponentLink()} > - Create Component + {buttonLabel ?? 'Create Component'} ); }; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 1596de0e8d..aca1db8940 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -18,6 +18,7 @@ export * from './components/AboutCard'; export { CatalogLayout } from './components/CatalogPage'; export { CatalogResultListItem } from './components/CatalogResultListItem'; export { CatalogTable } from './components/CatalogTable'; +export type { EntityRow } from './components/CatalogTable'; export { CreateComponentButton } from './components/CreateComponentButton'; export { EntityLayout } from './components/EntityLayout'; export * from './components/EntityOrphanWarning'; From 172c973247f64c62e21298e5ca58d5c55f9ca7b8 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Thu, 10 Jun 2021 13:00:43 -0400 Subject: [PATCH 129/206] feat(pickers): implement EntityLifecyclePicker and EntityOwnerPicker Signed-off-by: Phil Kuang --- .changeset/chilly-ants-taste.md | 5 + .../ApiExplorerPage/ApiExplorerPage.tsx | 4 + .../EntityLifecyclePicker.test.tsx | 139 ++++++++++++++++++ .../EntityLifecyclePicker.tsx | 85 +++++++++++ .../components/EntityLifecyclePicker/index.ts | 17 +++ .../EntityOwnerPicker.test.tsx | 139 ++++++++++++++++++ .../EntityOwnerPicker/EntityOwnerPicker.tsx | 83 +++++++++++ .../src/components/EntityOwnerPicker/index.ts | 17 +++ plugins/catalog-react/src/components/index.ts | 2 + .../src/hooks/useEntityListProvider.tsx | 4 + plugins/catalog-react/src/types.ts | 16 ++ 11 files changed, 511 insertions(+) create mode 100644 .changeset/chilly-ants-taste.md create mode 100644 plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx create mode 100644 plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx create mode 100644 plugins/catalog-react/src/components/EntityLifecyclePicker/index.ts create mode 100644 plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx create mode 100644 plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx create mode 100644 plugins/catalog-react/src/components/EntityOwnerPicker/index.ts diff --git a/.changeset/chilly-ants-taste.md b/.changeset/chilly-ants-taste.md new file mode 100644 index 0000000000..2c68e374f8 --- /dev/null +++ b/.changeset/chilly-ants-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Implement a `EntityLifecyclePicker` and `EntityOwnerPicker` diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index 36cd988c8c..e5dffa7e73 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -22,7 +22,9 @@ import { } from '@backstage/core'; import { EntityKindPicker, + EntityLifecyclePicker, EntityListProvider, + EntityOwnerPicker, EntityTagPicker, EntityTypePicker, UserListFilterKind, @@ -71,6 +73,8 @@ export const ApiExplorerPage = ({
diff --git a/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx b/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx index 8c30c9f9be..23f28f50d1 100644 --- a/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx +++ b/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx @@ -21,10 +21,10 @@ import { useRouteRef } from '@backstage/core'; import { createComponentRouteRef } from '../../routes'; type CreateComponentButtonProps = { - buttonLabel?: string; + label?: string; }; export const CreateComponentButton = ({ - buttonLabel, + label, }: CreateComponentButtonProps) => { const createComponentLink = useRouteRef(createComponentRouteRef); @@ -37,7 +37,7 @@ export const CreateComponentButton = ({ color="primary" to={createComponentLink()} > - {buttonLabel ?? 'Create Component'} + {label ?? 'Create Component'} ); }; From 5d286716c94cd36a0b1864a3968ed279e54352ed Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Mon, 14 Jun 2021 17:07:18 -0400 Subject: [PATCH 131/206] refactor(EntityOwnerPicker): use ownedBy relation Signed-off-by: Phil Kuang --- .changeset/wild-ghosts-deny.md | 2 +- .../EntityOwnerPicker.test.tsx | 50 +++++++++++++++---- .../EntityOwnerPicker/EntityOwnerPicker.tsx | 10 +++- plugins/catalog-react/src/types.ts | 15 ++++-- 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/.changeset/wild-ghosts-deny.md b/.changeset/wild-ghosts-deny.md index 09be2d580c..b11eba92f3 100644 --- a/.changeset/wild-ghosts-deny.md +++ b/.changeset/wild-ghosts-deny.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog': patch --- -Export `EntityRow` type and `CreateComponentButton` components +Export `EntityRow` type diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx index 5473f3a5de..49188cbad6 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx @@ -28,9 +28,24 @@ const sampleEntities: Entity[] = [ metadata: { name: 'component-1', }, - spec: { - owner: 'some-owner', - }, + relations: [ + { + type: 'ownedBy', + target: { + name: 'some-owner', + namespace: 'default', + kind: 'Group', + }, + }, + { + type: 'ownedBy', + target: { + name: 'some-owner-2', + namespace: 'default', + kind: 'Group', + }, + }, + ], }, { apiVersion: '1', @@ -38,9 +53,16 @@ const sampleEntities: Entity[] = [ metadata: { name: 'component-2', }, - spec: { - owner: 'another-owner', - }, + relations: [ + { + type: 'ownedBy', + target: { + name: 'another-owner', + namespace: 'default', + kind: 'Group', + }, + }, + ], }, { apiVersion: '1', @@ -48,9 +70,16 @@ const sampleEntities: Entity[] = [ metadata: { name: 'component-3', }, - spec: { - owner: 'some-owner', - }, + relations: [ + { + type: 'ownedBy', + target: { + name: 'some-owner', + namespace: 'default', + kind: 'Group', + }, + }, + ], }, ]; @@ -67,7 +96,7 @@ describe('', () => { fireEvent.click(rendered.getByTestId('owner-picker-expand')); sampleEntities - .map(e => e.spec?.owner!) + .flatMap(e => e.relations?.map(r => r.target.name)) .forEach(owner => { expect(rendered.getByText(owner as string)).toBeInTheDocument(); }); @@ -88,6 +117,7 @@ describe('', () => { expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([ 'another-owner', 'some-owner', + 'some-owner-2', ]); }); diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index c46affe86b..65fba2150c 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { Box, Checkbox, @@ -29,6 +29,8 @@ import { Autocomplete } from '@material-ui/lab'; import React, { useMemo } from 'react'; import { useEntityListProvider } from '../../hooks/useEntityListProvider'; import { EntityOwnerFilter } from '../../types'; +import { getEntityRelations } from '../../utils'; +import { formatEntityRefTitle } from '../EntityRefLink'; const icon = ; const checkedIcon = ; @@ -40,7 +42,11 @@ export const EntityOwnerPicker = () => { [ ...new Set( backendEntities - .map((e: Entity) => e.spec?.owner) + .flatMap((e: Entity) => + getEntityRelations(e, RELATION_OWNED_BY).map(o => + formatEntityRefTitle(o, { defaultKind: 'group' }), + ), + ) .filter(Boolean) as string[], ), ].sort(), diff --git a/plugins/catalog-react/src/types.ts b/plugins/catalog-react/src/types.ts index 07abc9d332..9ede73baba 100644 --- a/plugins/catalog-react/src/types.ts +++ b/plugins/catalog-react/src/types.ts @@ -14,8 +14,13 @@ * limitations under the License. */ -import { Entity, UserEntity } from '@backstage/catalog-model'; -import { isOwnerOf } from './utils'; +import { + Entity, + RELATION_OWNED_BY, + UserEntity, +} from '@backstage/catalog-model'; +import { getEntityRelations, isOwnerOf } from './utils'; +import { formatEntityRefTitle } from './components/EntityRefLink'; export type EntityFilter = { /** @@ -65,7 +70,11 @@ export class EntityOwnerFilter implements EntityFilter { constructor(readonly values: string[]) {} filterEntity(entity: Entity): boolean { - return this.values.some(v => entity.spec?.owner === v); + return this.values.some(v => + getEntityRelations(entity, RELATION_OWNED_BY).some( + o => formatEntityRefTitle(o, { defaultKind: 'group' }) === v, + ), + ); } } From 1753dae28aad35a9a42a2f4aec90e74011514929 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Tue, 15 Jun 2021 17:50:25 +0100 Subject: [PATCH 132/206] fix: limit database manager exports and update changeset Signed-off-by: Minn Soe --- .changeset/five-donkeys-brake.md | 6 +++--- packages/backend-common/api-report.md | 8 -------- packages/backend-common/src/database/index.ts | 3 ++- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md index b0bb635876..274e704888 100644 --- a/.changeset/five-donkeys-brake.md +++ b/.changeset/five-donkeys-brake.md @@ -1,7 +1,7 @@ --- -'@backstage/backend-common': minor -'@backstage/create-app': minor -'@backstage/backend-test-utils': minor +'@backstage/backend-common': patch +'@backstage/create-app': patch +'@backstage/backend-test-utils': patch --- Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 1cf1f4f21b..ed78157810 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -104,14 +104,6 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl; // @public (undocumented) export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise; -// @public -export interface DatabaseConnector { - createClient(dbConfig: Config, overrides?: Partial): Knex; - createNameOverride(name: string): Partial; - ensureDatabaseExists?(dbConfig: Config, ...databases: Array): Promise; - parseConnectionString(connectionString: string, client?: string): Knex.StaticConnectionConfig; -} - // @public (undocumented) export class DatabaseManager { forPlugin(pluginId: string): PluginDatabaseManager; diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 7dfb08359b..f8957907f0 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -15,6 +15,7 @@ */ export * from './connection'; -export * from './types'; export * from './SingleConnection'; export * from './DatabaseManager'; + +export type { PluginDatabaseManager } from './types'; From 46e9e44541dc5ba7534c4a0f6190e323026886dd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 15 Jun 2021 19:00:42 +0200 Subject: [PATCH 133/206] Update flat-dolls-search.md Signed-off-by: Patrik Oldsberg --- .changeset/flat-dolls-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/flat-dolls-search.md b/.changeset/flat-dolls-search.md index 02a7124cb7..4c09b64168 100644 --- a/.changeset/flat-dolls-search.md +++ b/.changeset/flat-dolls-search.md @@ -2,4 +2,4 @@ '@backstage/plugin-todo-backend': patch --- -Bump leasot dependency from 11.5.0 to 12.0.0, removing support for Node.js version 10. +Bump `leasot` dependency from 11.5.0 to 12.0.0, removing support for Node.js version 10. From fb4a7f71e66114268dab6ac506179d50a3587b10 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Tue, 15 Jun 2021 13:18:05 -0400 Subject: [PATCH 134/206] revert(CreateComponentButton): use dedicated button in api page Signed-off-by: Phil Kuang --- .changeset/wild-ghosts-deny.md | 2 +- .../ApiExplorerPage/ApiExplorerPage.tsx | 26 ++++++++++++------- .../CreateComponentButton.tsx | 9 ++----- plugins/catalog/src/index.ts | 2 +- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.changeset/wild-ghosts-deny.md b/.changeset/wild-ghosts-deny.md index b11eba92f3..efd10dbc25 100644 --- a/.changeset/wild-ghosts-deny.md +++ b/.changeset/wild-ghosts-deny.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog': patch --- -Export `EntityRow` type +Export `CatalogTableRow` type diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index 163aad4cab..6d551ea0c6 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -19,6 +19,7 @@ import { ContentHeader, SupportButton, TableColumn, + useRouteRef, } from '@backstage/core'; import { EntityKindPicker, @@ -30,14 +31,11 @@ import { UserListFilterKind, UserListPicker, } from '@backstage/plugin-catalog-react'; -import { - CatalogTable, - CreateComponentButton, - EntityRow, -} from '@backstage/plugin-catalog'; -import { makeStyles } from '@material-ui/core'; - +import { CatalogTable, CatalogTableRow } from '@backstage/plugin-catalog'; +import { Button, makeStyles } from '@material-ui/core'; import React from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { createComponentRouteRef } from '../../routes'; import { ApiExplorerLayout } from './ApiExplorerLayout'; const useStyles = makeStyles(theme => ({ @@ -51,7 +49,7 @@ const useStyles = makeStyles(theme => ({ export type ApiExplorerPageProps = { initiallySelectedFilter?: UserListFilterKind; - columns?: TableColumn[]; + columns?: TableColumn[]; }; export const ApiExplorerPage = ({ @@ -59,12 +57,22 @@ export const ApiExplorerPage = ({ columns, }: ApiExplorerPageProps) => { const styles = useStyles(); + const createComponentLink = useRouteRef(createComponentRouteRef); return ( - + {createComponentLink && ( + + )} All your APIs
diff --git a/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx b/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx index 23f28f50d1..896e9bada3 100644 --- a/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx +++ b/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx @@ -20,12 +20,7 @@ import { Button } from '@material-ui/core'; import { useRouteRef } from '@backstage/core'; import { createComponentRouteRef } from '../../routes'; -type CreateComponentButtonProps = { - label?: string; -}; -export const CreateComponentButton = ({ - label, -}: CreateComponentButtonProps) => { +export const CreateComponentButton = () => { const createComponentLink = useRouteRef(createComponentRouteRef); if (!createComponentLink) return null; @@ -37,7 +32,7 @@ export const CreateComponentButton = ({ color="primary" to={createComponentLink()} > - {label ?? 'Create Component'} + Create Component ); }; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index aca1db8940..0a40d50036 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -18,7 +18,7 @@ export * from './components/AboutCard'; export { CatalogLayout } from './components/CatalogPage'; export { CatalogResultListItem } from './components/CatalogResultListItem'; export { CatalogTable } from './components/CatalogTable'; -export type { EntityRow } from './components/CatalogTable'; +export type { EntityRow as CatalogTableRow } from './components/CatalogTable'; export { CreateComponentButton } from './components/CreateComponentButton'; export { EntityLayout } from './components/EntityLayout'; export * from './components/EntityOrphanWarning'; From 78830d3b7eb756e82b402981fdda35d36fef1b5e Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Tue, 15 Jun 2021 18:21:12 +0100 Subject: [PATCH 135/206] fix: limit helpers from db manager connections Signed-off-by: Minn Soe --- packages/backend-common/api-report.md | 10 ---------- packages/backend-common/src/database/index.ts | 11 ++++++++++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index ed78157810..fcddb8dda5 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -16,7 +16,6 @@ import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegration } from '@backstage/integration'; import { GitLabIntegration } from '@backstage/integration'; import * as http from 'http'; -import { JsonObject } from '@backstage/config'; import { JsonValue } from '@backstage/config'; import { Knex } from 'knex'; import { Logger } from 'winston'; @@ -92,9 +91,6 @@ export const createDatabase: typeof createDatabaseClient; // @public export function createDatabaseClient(dbConfig: Config, overrides?: Partial): Knex; -// @public -export function createNameOverride(client: string, name: string): Partial; - // @public (undocumented) export function createRootLogger(options?: winston.LoggerOptions, env?: NodeJS.ProcessEnv): winston.Logger; @@ -259,15 +255,9 @@ export class GitlabUrlReader implements UrlReader { // @public export function loadBackendConfig(options: Options): Promise; -// @public -export function normalizeConnection(connection: Knex.StaticConnectionConfig | JsonObject | string | undefined, client: string): Partial; - // @public export function notFoundHandler(): RequestHandler; -// @public -export function parseConnectionString(connectionString: string, client?: string): Knex.StaticConnectionConfig; - // @public export type PluginCacheManager = { getClient: (options?: ClientOptions) => CacheClient; diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index f8957907f0..bfb14e7353 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -14,8 +14,17 @@ * limitations under the License. */ -export * from './connection'; export * from './SingleConnection'; export * from './DatabaseManager'; +/* + * Undocumented API surface from connection is being reduced for future deprecation. + * Avoid exporting additional symbols. + */ +export { + createDatabaseClient, + createDatabase, + ensureDatabaseExists, +} from './connection'; + export type { PluginDatabaseManager } from './types'; From 287319dbf88e39b4819ebb55cfc66d1d56fee834 Mon Sep 17 00:00:00 2001 From: Fabian Hippmann Date: Tue, 15 Jun 2021 19:49:35 +0200 Subject: [PATCH 136/206] fix adopters linting Signed-off-by: Fabian Hippmann --- ADOPTERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index ceb6f20d31..4abab25e30 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -30,4 +30,4 @@ | [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | | [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | | [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | From 785a42f802b512be3813bc3be035608b0fd7fbc8 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 15 Jun 2021 15:00:15 -0600 Subject: [PATCH 137/206] Move installation instructions to READMEs Signed-off-by: Tim Hansen --- docs/features/software-catalog/index.md | 5 +- .../features/software-catalog/installation.md | 177 ----------- .../software-templates/configuration.md | 52 ++++ docs/features/software-templates/index.md | 6 +- .../software-templates/installation.md | 280 ------------------ .../techdocs/creating-and-publishing.md | 8 +- docs/plugins/github-apps.md | 9 + microsite/sidebars.json | 3 +- mkdocs.yml | 3 +- plugins/catalog-backend/README.md | 88 +++++- plugins/catalog/README.md | 105 ++++++- plugins/scaffolder-backend/README.md | 67 ++++- plugins/scaffolder/README.md | 84 +++++- 13 files changed, 371 insertions(+), 516 deletions(-) delete mode 100644 docs/features/software-catalog/installation.md create mode 100644 docs/features/software-templates/configuration.md delete mode 100644 docs/features/software-templates/installation.md diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md index 70541b85db..2189b7d799 100644 --- a/docs/features/software-catalog/index.md +++ b/docs/features/software-catalog/index.md @@ -34,9 +34,8 @@ More specifically, the Service Catalog enables two main use-cases: ## Getting Started The Software Catalog is available to browse at `/catalog`. If you've followed -[Installing in your Backstage App](./installation.md) in your separate App or -[Getting Started with Backstage](../../getting-started) for this repo, you -should be able to browse the catalog at `http://localhost:3000`. +[Getting Started with Backstage](../../getting-started), you should be able to +browse the catalog at `http://localhost:3000`. ![](../../assets/software-catalog/service-catalog-home.png) diff --git a/docs/features/software-catalog/installation.md b/docs/features/software-catalog/installation.md deleted file mode 100644 index 0b622fb093..0000000000 --- a/docs/features/software-catalog/installation.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -id: installation -title: Installing in your Backstage App -description: Documentation on How to install Backstage Plugin ---- - -The catalog plugin comes in two packages, `@backstage/plugin-catalog` and -`@backstage/plugin-catalog-backend`. Each has their own installation steps, -outlined below. - -## Installing @backstage/plugin-catalog - -> **Note that if you used `npx @backstage/create-app`, the plugin is already -> installed and you can skip to -> [adding entries to the catalog](#adding-entries-to-the-catalog)** - -The catalog frontend plugin should be installed in your `app` package, which is -created as a part of `@backstage/create-app`. To install the package, run: - -```bash -# From your Backstage root directory -cd packages/app -yarn add @backstage/plugin-catalog -``` - -### Adding the Plugin to your `packages/app` - -Add the two pages that the catalog plugin provides to your app. You can choose -any name for these routes, but we recommend the following: - -```tsx -// packages/app/src/App.tsx -import { - catalogPlugin, - CatalogIndexPage, - CatalogEntityPage, -} from '@backstage/plugin-catalog'; - -// Add to the top-level routes, directly within -} /> -}> - {/* - This is the root of the custom entity pages for your app, refer to the example app - in the main repo or the output of @backstage/create-app for an example - */} - - -``` - -The catalog plugin also has one external route that needs to be bound for it to -function: the `createComponent` route which should link to the page where the -user can create components. In a typical setup the create component route will -be linked to the Scaffolder plugin's template index page: - -```ts -// packages/app/src/App.tsx -import { catalogPlugin } from '@backstage/plugin-catalog'; -import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; - -const app = createApp({ - // ... - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - }); - }, -}); -``` - -You may also want to add a link to the catalog index page to your sidebar: - -```tsx -// packages/app/src/components/Root.tsx -import HomeIcon from '@material-ui/icons/Home'; - -// Somewhere within the -; -``` - -This is all that is needed for the frontend part of the Catalog plugin to work! - -## Gotchas that we will fix - -Since the catalog plugin currently ships with a sentry plugin `InfoCard` -installed by default, you'll need to set `sentry.organization` in your -`app-config.yaml`. For example: - -```yaml -sentry: - organization: Acme Corporation -``` - -If you've created an app with an older version of `@backstage/create-app` or -`@backstage/cli create-app`, be sure to remove the Welcome plugin from the app, -as that will conflict with the catalog routes. - -## Installing @backstage/plugin-catalog-backend - -> **Note that if you used `npx @backstage/create-app`, the plugin is already -> installed and you can skip to -> [adding entries to the catalog](#adding-entries-to-the-catalog)** - -The catalog backend should be installed in your `backend` package, which is -created as a part of `@backstage/create-app`. To install the package, run: - -```bash -# From your Backstage root directory -cd packages/backend -yarn add @backstage/plugin-catalog-backend -``` - -### Adding the Plugin to your `packages/backend` - -You'll need to add the plugin to the `backend`'s router. You can do this by -creating a file called `packages/backend/src/plugins/catalog.ts` with contents -matching -[catalog.ts in the create-app template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts). - -Once the `catalog.ts` router setup file is in place, add the router to -`packages/backend/src/index.ts`: - -```ts -import catalog from './plugins/catalog'; - -const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); - -const apiRouter = Router(); -/** several different routers */ -apiRouter.use('/catalog', await catalog(catalogEnv)); -``` - -### Adding Entries to the Catalog - -At this point the catalog backend is installed in your backend package, but you -will not have any entities loaded. - -To get up and running and try out some templates quickly, you can add some of -our example templates through static configuration. Add the following to the -`catalog.locations` section in your `app-config.yaml`: - -```yaml -catalog: - locations: - # Backstage Example Components - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/artist-lookup-component.yaml - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/playback-order-component.yaml - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/podcast-api-component.yaml - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/queue-proxy-component.yaml - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/searcher-component.yaml - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/playback-lib-component.yaml - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/www-artist-component.yaml - - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/shuffle-api-component.yaml -``` - -### Running the Backend - -Finally, start up Backstage with the new configuration: - -```bash -# Run from the root to start both backend and frontend -yarn dev - -# Alternatively, run only the backend from its own package -cd packages/backend -yarn start -``` - -If you've also set up the frontend plugin, you should be ready to go browse the -catalog at [localhost:3000](http://localhost:3000) now! diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md new file mode 100644 index 0000000000..cdd8166889 --- /dev/null +++ b/docs/features/software-templates/configuration.md @@ -0,0 +1,52 @@ +--- +id: configuration +title: Software Template Configuration +sidebar_label: Configuration +description: Configuration options for Backstage Software Templates +--- + +Backstage software templates create source code, so your Backstage application +needs to be set up to allow repository creation. + +This is done in your `app-config.yaml` by adding +[Backstage integrations](https://backstage.io/docs/integrations/) for the +appropriate source code repository for your organization. + +> Note: Integrations may already be set up as part of your `app-config.yaml`. + +The next step is to add +[add templates](http://backstage.io/docs/features/software-templates/adding-templates) +to your Backstage app. + +### GitHub + +For GitHub, you can configure who can see the new repositories that are created +by specifying `visibility` option. Valid options are `public`, `private` and +`internal`. The `internal` option is for GitHub Enterprise clients, which means +public within the enterprise. + +```yaml +scaffolder: + github: + visibility: public # or 'internal' or 'private' +``` + +### Disabling Docker in Docker situation (Optional) + +Software Templates use +[Cookiecutter](https://github.com/cookiecutter/cookiecutter) as a templating +library. By default it will use the +[scaffolder-backend/Cookiecutter](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile) +docker image. + +If you are running Backstage from a Docker container and you want to avoid +calling a container inside a container, you can set up Cookiecutter in your own +image, this will use the local installation instead. + +You can do so by including the following lines in the last step of your +`Dockerfile`: + +```Dockerfile +RUN apt-get update && apt-get install -y python3 python3-pip +RUN pip3 install cookiecutter +``` diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 12ce6e3ed3..1434d62a29 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -17,10 +17,8 @@ locations like GitHub or GitLab. ### Getting Started -> Be sure to have covered [Installing in your Backstage App](./installation.md) -> for your separate App or -> [Getting Started with Backstage](../../getting-started) for this repo before -> proceeding. +> Be sure to have covered +> [Getting Started with Backstage](../../getting-started) before proceeding. The Software Templates are available under `/create`. For local development you should be able to reach them at `http://localhost:3000/create`. diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md deleted file mode 100644 index d5643ca955..0000000000 --- a/docs/features/software-templates/installation.md +++ /dev/null @@ -1,280 +0,0 @@ ---- -id: installation -title: Installing in your Backstage App -description: Documentation on How to install Backstage App ---- - -The scaffolder plugin comes in two packages, `@backstage/plugin-scaffolder` and -`@backstage/plugin-scaffolder-backend`. Each has their own installation steps, -outlined below. - -The Scaffolder plugin also depends on the Software Catalog. Instructions for how -to set that up can be found [here](../software-catalog/installation.md). - -## Installing @backstage/plugin-scaffolder - -> **Note that if you used `npx @backstage/create-app`, the plugin may already be -> present** - -The scaffolder frontend plugin should be installed in your `app` package, which -is created as a part of `@backstage/create-app`. To install the package, run: - -```bash -# From your Backstage root directory -cd packages/app -yarn add @backstage/plugin-scaffolder -``` - -### Adding the Plugin to your `packages/app` - -Add the root page that the Scaffolder plugin provides to your app. You can -choose any path for the route, but we recommend the following: - -```tsx -import { ScaffolderPage } from '@backstage/plugin-scaffolder'; - -// Add to the top-level routes, directly within -} />; -``` - -You may also want to add a link to the template index page to your sidebar: - -```tsx -import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; - -// Somewhere within the -; -``` - -This is all that is needed for the frontend part of the Scaffolder plugin to -work! - -## Installing @backstage/plugin-scaffolder-backend - -> **Note that if you used `npx @backstage/create-app`, the plugin may already be -> present** - -The scaffolder backend should be installed in your `backend` package, which is -created as a part of `@backstage/create-app`. To install the package, run: - -```bash -# From your Backstage root directory -cd packages/backend -yarn add @backstage/plugin-scaffolder-backend -``` - -### Adding the Plugin to your `packages/backend` - -You'll need to add the plugin to the `backend`'s router. You can do this by -creating a file called `packages/backend/src/plugins/scaffolder.ts` with the -following contents to get you up and running quickly. - -```ts -import { - DockerContainerRunner, - SingleHostDiscovery, -} from '@backstage/backend-common'; -import { - CookieCutter, - createRouter, - Preparers, - Publishers, - CreateReactAppTemplater, - Templaters, -} from '@backstage/plugin-scaffolder-backend'; -import type { PluginEnvironment } from '../types'; -import Docker from 'dockerode'; -import { CatalogClient } from '@backstage/catalog-client'; - -export default async function createPlugin({ - logger, - config, - database, - reader, -}: PluginEnvironment) { - const dockerClient = new Docker(); - const containerRunner = new DockerContainerRunner({ dockerClient }); - - const cookiecutterTemplater = new CookieCutter({ containerRunner }); - const craTemplater = new CreateReactAppTemplater({ containerRunner }); - const templaters = new Templaters(); - - templaters.register('cookiecutter', cookiecutterTemplater); - templaters.register('cra', craTemplater); - - const preparers = await Preparers.fromConfig(config, { logger }); - const publishers = await Publishers.fromConfig(config, { logger }); - - const discovery = SingleHostDiscovery.fromConfig(config); - const catalogClient = new CatalogClient({ discoveryApi: discovery }); - - return await createRouter({ - preparers, - templaters, - publishers, - logger, - config, - database, - catalogClient, - reader, - }); -} -``` - -Once the `scaffolder.ts` router setup file is in place, add the router to -`packages/backend/src/index.ts`: - -```ts -import scaffolder from './plugins/scaffolder'; - -const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); - -const apiRouter = Router(); -/* several router .use calls */ - -/* add this line */ -apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); -``` - -### Adding Templates - -At this point the scaffolder backend is installed in your backend package, but -you will not have any templates available to use. These need to be added to the -software catalog, as they are represented as entities of kind -[Template](../software-catalog/descriptor-format.md#kind-template). You can find -out more about adding templates [here](./adding-templates.md). - -To get up and running and try out some templates quickly, you can add some of -our example templates through static configuration. Add the following to the -`catalog.locations` section in your `app-config.yaml`: - -```yaml -catalog: - locations: - # Backstage Example Templates - - type: url - target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml - - type: url - target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml - - type: url - target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml - - type: url - target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml -``` - -### Runtime Dependencies / Configuration - -For the scaffolder backend plugin to function, you'll need to setup the -integrations config in your `app-config.yaml`. - -You can find help for different providers below. - -> Note: Some of this configuration may already be set up as part of your -> `app-config.yaml`. We're moving away from the duplicated config for -> authentication in the `scaffolder` section and using `integrations` instead. - -#### GitHub - -The GitHub access token is retrieved from environment variables via the config. -The config file needs to specify what environment variable the token is -retrieved from. Your config should have the following objects. - -You can configure who can see the new repositories that the scaffolder creates -by specifying `visibility` option. Valid options are `public`, `private` and -`internal`. The `internal` option is for GitHub Enterprise clients, which means -public within the enterprise. - -```yaml -integrations: - github: - - host: github.com - token: ${GITHUB_TOKEN} - -scaffolder: - github: - visibility: public # or 'internal' or 'private' -``` - -#### GitLab - -For GitLab, we currently support the configuration of the GitLab publisher and -allows to configure the private access token and the base URL of a GitLab -instance: - -```yaml -integrations: - gitlab: - - host: gitlab.com - token: ${GITLAB_TOKEN} -``` - -#### Bitbucket - -For Bitbucket there are two authentication methods supported. Either `token` or -a combination of `appPassword` and `username`. It looks like either of the -following: - -```yaml -integrations: - bitbucket: - - host: bitbucket.org - token: ${BITBUCKET_TOKEN} -``` - -or - -```yaml -integrations: - bitbucket: - - host: bitbucket.org - appPassword: ${BITBUCKET_APP_PASSWORD} - username: ${BITBUCKET_USERNAME} -``` - -#### Azure DevOps - -For Azure DevOps we support both the preparer and publisher stage with the -configuration of a private access token (PAT). For the publisher it's also -required to define the base URL for the client to connect to the service. This -will hopefully support on-prem installations as well but that has not been -verified. - -```yaml -integrations: - azure: - - host: dev.azure.com - token: ${AZURE_TOKEN} -``` - -### Running the Backend - -Finally, make sure you have a local Docker daemon running, and start up the -backend with the new configuration: - -```bash -cd packages/backend -GITHUB_TOKEN= yarn start -``` - -If you've also set up the frontend plugin, so you should be ready to go browse -the templates at [localhost:3000/create](http://localhost:3000/create) now! - -### Disabling Docker in Docker situation (Optional) - -Software Templates use -[Cookiecutter](https://github.com/cookiecutter/cookiecutter) as templating -library. By default it will use the -[spotify/backstage-cookiecutter](https://github.com/backstage/backstage/blob/37e35b910afc7d1270855aed0ec4718aba366c91/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile) -docker image. - -If you are running Backstage from a Docker container and you want to avoid -calling a container inside a container, you can set up Cookiecutter in your own -image, this will use the local installation instead. - -You can do so by including the following lines in the last step of your -`Dockerfile`: - -```Dockerfile -RUN apt-get update && apt-get install -y python3 python3-pip -RUN pip3 install cookiecutter -``` diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index b9340330a8..3fdc942ac4 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -28,10 +28,10 @@ scratch. ### Use the documentation template Your working Backstage instance should by default have a documentation template -added. If not, follow these -[instructions](../software-templates/installation.md#adding-templates) to add -the documentation template. The template creates a component with only TechDocs -configuration and default markdown files as below mentioned in manual +added. If not, copy the catalog locations from the +[create-app template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs) +to add the documentation template. The template creates a component with only +TechDocs configuration and default markdown files as below mentioned in manual documentation setup, and is otherwise empty. ![Documentation Template](../../assets/techdocs/documentation-template.png) diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md index 87d23b45d5..b7ad60b12f 100644 --- a/docs/plugins/github-apps.md +++ b/docs/plugins/github-apps.md @@ -84,3 +84,12 @@ integrations: apps: - $include: example-backstage-app-credentials.yaml ``` + +### Permissions for pull requests + +These are the minimum permissions required for creating a pull request with +Backstage software templates: + +- Read and Write permissions for `Contents`. +- Read and write permissions for `Pull Requests` and `Issues`. +- Read permissions on `Metadata`. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index f2ce425307..6caf14e854 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -33,7 +33,6 @@ "label": "Software Catalog", "ids": [ "features/software-catalog/software-catalog-overview", - "features/software-catalog/installation", "features/software-catalog/configuration", "features/software-catalog/system-model", "features/software-catalog/descriptor-format", @@ -62,7 +61,7 @@ "label": "Software Templates", "ids": [ "features/software-templates/software-templates-index", - "features/software-templates/installation", + "features/software-templates/configuration", "features/software-templates/adding-templates", "features/software-templates/writing-templates", "features/software-templates/builtin-actions", diff --git a/mkdocs.yml b/mkdocs.yml index 67b97b6ffb..c62824d39e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,7 +30,6 @@ nav: - Core Features: - Software Catalog: - Overview: 'features/software-catalog/index.md' - - Installing in your Backstage App: 'features/software-catalog/installation.md' - Catalog Configuration: 'features/software-catalog/configuration.md' - System Model: 'features/software-catalog/system-model.md' - YAML File Format: 'features/software-catalog/descriptor-format.md' @@ -49,7 +48,7 @@ nav: - Troubleshooting: 'features/kubernetes/troubleshooting.md' - Software Templates: - Overview: 'features/software-templates/index.md' - - Installing in your Backstage App: 'features/software-templates/installation.md' + - Configuration: 'features/software-templates/configuration.md' - Adding your own Templates: 'features/software-templates/adding-templates.md' - Writing Templates: 'features/software-templates/writing-templates.md' - Builtin Actions: 'features/software-templates/builtin-actions.md' diff --git a/plugins/catalog-backend/README.md b/plugins/catalog-backend/README.md index 2f06b9c062..ce97bb7a54 100644 --- a/plugins/catalog-backend/README.md +++ b/plugins/catalog-backend/README.md @@ -1,21 +1,80 @@ # Catalog Backend -This is the backend part of the default catalog plugin. +This is the backend for the default Backstage [software +catalog](http://backstage.io/docs/features/software-catalog/software-catalog-overview). +This provides an API for consumers such as the frontend [catalog +plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog). -It comes with a builtin database backed implementation of the catalog, that can store -and serve your catalog for you. +It comes with a builtin database-backed implementation of the catalog that can +store and serve your catalog for you. -It can also act as a bridge to your existing catalog solutions, either ingesting their -data to store in the database, or by effectively proxying calls to an external catalog -service. +It can also act as a bridge to your existing catalog solutions, either ingesting +data to store in the database, or by effectively proxying calls to an +external catalog service. -## Getting Started +## Installation -This backend plugin can be started in a standalone mode from directly in this package -with `yarn start`. However, it will have limited functionality and that process is -most convenient when developing the catalog backend plugin itself. +This `@backstage/plugin-catalog-backend` package comes installed by default in +any Backstage application created with `npx @backstage/create-app`, so +installation is not usually required. -To evaluate the catalog and have a greater amount of functionality available, instead do +To check if you already have the package, look under +`packages/backend/package.json`, in the `dependencies` block, for +`@backstage/plugin-catalog-backend`. The instructions below walk through +restoring the plugin, if you previously removed it. + +### Install the package + +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-catalog-backend +``` + +### Adding the plugin to your `packages/backend` + +You'll need to add the plugin to the router in your `backend` package. You can +do this by creating a file called `packages/backend/src/plugins/catalog.ts` with +contents matching [catalog.ts in the create-app +template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts). + +With the `catalog.ts` router setup in place, add the router to +`packages/backend/src/index.ts`: + +```diff ++import catalog from './plugins/catalog'; + +async function main() { + ... + const createEnv = makeCreateEnv(config); + ++ const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); + const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); + + const apiRouter = Router(); ++ apiRouter.use('/catalog', await catalog(catalogEnv)); + ... + apiRouter.use(notFoundHandler()); + +``` + +### Adding catalog entities + +At this point the `catalog-backend` is installed in your backend package, but +you will not have any catalog entities loaded. See [Catalog +Configuration](https://backstage.io/docs/features/software-catalog/configuration) +for how to add locations, or copy the catalog locations from the [create-app +template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs) +to get up and running quickly. + +## Development + +This backend plugin can be started in a standalone mode from directly in this +package with `yarn start`. However, it will have limited functionality and that +process is most convenient when developing the catalog backend plugin itself. + +To evaluate the catalog and have a greater amount of functionality available, +run the entire Backstage example application from the root folder: ```bash # in one terminal window, run this from from the very root of the Backstage project @@ -23,9 +82,10 @@ cd packages/backend yarn start ``` -This will launch the full example backend, populated some example entities. +This will launch both frontend and backend in the same window, populated with +some example entities. ## Links -- [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog) -- [The Backstage homepage](https://backstage.io) +- [catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend) + is the frontend interface for this plugin. diff --git a/plugins/catalog/README.md b/plugins/catalog/README.md index 611d2989e8..8527c8da7c 100644 --- a/plugins/catalog/README.md +++ b/plugins/catalog/README.md @@ -1,26 +1,107 @@ # Backstage Catalog Frontend -This is the frontend part of the default catalog plugin. +This is the React frontend for the default Backstage [software +catalog](http://backstage.io/docs/features/software-catalog/software-catalog-overview). +This package supplies interfaces related to listing catalog entities or showing +more information about them on entity pages. -It will implement the core API for handling your catalog of software, and -supply the base views to show and manage them. +## Installation -## Getting Started +This `@backstage/plugin-catalog` package comes installed by default in any +Backstage application created with `npx @backstage/create-app`, so installation +is not usually required. -This frontend plugin can be started in a standalone mode from directly in this package -with `yarn start`. However, it will have limited functionality and that process is -most convenient when developing the catalog frontend plugin itself. +To check if you already have the package, look under +`packages/app/package.json`, in the `dependencies` block, for +`@backstage/plugin-catalog`. The instructions below walk through restoring the +plugin, if you previously removed it. -To evaluate the catalog and have a greater amount of functionality available, from the main -Backstage root folder, instead do: +### Install the package + +```bash +# From your Backstage root directory +cd packages/app +yarn add @backstage/plugin-catalog +``` + +### Add the plugin to your `packages/app` + +Add the two pages that the catalog plugin provides to your app. You can choose +any name for these routes, but we recommend the following: + +```diff +// packages/app/src/App.tsx +import { + CatalogIndexPage, + CatalogEntityPage, +} from '@backstage/plugin-catalog'; +import { entityPage } from './components/catalog/EntityPage'; + + ++ } /> ++ }> ++ {/* ++ This is the root of the custom entity pages for your app, refer to the example app ++ in the main repo or the output of @backstage/create-app for an example ++ */} ++ {entityPage} ++ + ... + +``` + +The catalog plugin also has one external route that needs to be bound for it to +function: the `createComponent` route which should link to the page where the +user can create components. In a typical setup the create component route will +be linked to the scaffolder plugin's template index page: + +```diff +// packages/app/src/App.tsx ++import { catalogPlugin } from '@backstage/plugin-catalog'; ++import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; + +const app = createApp({ + // ... + bindRoutes({ bind }) { ++ bind(catalogPlugin.externalRoutes, { ++ createComponent: scaffolderPlugin.routes.root, ++ }); + }, +}); +``` + +You may also want to add a link to the catalog index page to your application +sidebar: + +```diff +// packages/app/src/components/Root/Root.tsx ++import HomeIcon from '@material-ui/icons/Home'; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + ++ + ... + +``` + +## Development + +This frontend plugin can be started in a standalone mode from directly in this +package with `yarn start`. However, it will have limited functionality and that +process is most convenient when developing the catalog frontend plugin itself. + +To evaluate the catalog and have a greater amount of functionality available, +run the entire Backstage example application from the root folder: ```bash yarn dev ``` -This will launch both frontend and backend in the same window, populated with some example entities. +This will launch both frontend and backend in the same window, populated with +some example entities. ## Links -- [Backend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend) -- [The Backstage homepage](https://backstage.io) +- [catalog-backend](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend) + provides the backend API for this frontend. diff --git a/plugins/scaffolder-backend/README.md b/plugins/scaffolder-backend/README.md index 304d43a750..7efe33f7f6 100644 --- a/plugins/scaffolder-backend/README.md +++ b/plugins/scaffolder-backend/README.md @@ -1,25 +1,64 @@ # Scaffolder Backend -Welcome to the scaffolder plugin! +This is the backend for the default Backstage [software +templates](https://backstage.io/docs/features/software-templates/software-templates-index). +This provides the API for the frontend [scaffolder +plugin](https://github.com/backstage/backstage/tree/master/plugins/scaffolder), +as well as the built-in template actions, tasks and stages. -## Jobs +## Installation -Documentation for `Jobs` here +This `@backstage/plugin-scaffolder-backend` package comes installed by default +in any Backstage application created with `npx @backstage/create-app`, so +installation is not usually required. -## Stages +To check if you already have the package, look under +`packages/backend/package.json`, in the `dependencies` block, for +`@backstage/plugin-scaffolder-backend`. The instructions below walk through +restoring the plugin, if you previously removed it. -Documentation for `Stages` here +### Install the package -## Tasks +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-scaffolder-backend +``` -Documentation for `Tasks` here +### Adding the plugin to your `packages/backend` -## Actions +You'll need to add the plugin to the router in your `backend` package. You can +do this by creating a file called `packages/backend/src/plugins/scaffolder.ts` +with contents matching [scaffolder.ts in the create-app +template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts). -### Built-in: +With the `scaffolder.ts` router setup in place, add the router to +`packages/backend/src/index.ts`: -- #### GitHub Pull Request - - Minimum permissions required for GitHub App for creating a Pull Request with the built-in action: - - Read and Write permissions for `Contents`. - - Read and write permissions for `Pull Requests` and `Issues`. - - Read permissions on `Metadata`. +```diff ++import scaffolder from './plugins/scaffolder'; + +async function main() { + ... + const createEnv = makeCreateEnv(config); + + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); ++ const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); + + const apiRouter = Router(); ++ apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); + ... + apiRouter.use(notFoundHandler()); + +``` + +### Adding templates + +At this point the scaffolder backend is installed in your backend package, but +you will not have any templates available to use. These need to be [added to the +software +catalog](https://backstage.io/docs/features/software-templates/adding-templates). + +To get up and running and try out some templates quickly, you can or copy the +catalog locations from the [create-app +template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs). diff --git a/plugins/scaffolder/README.md b/plugins/scaffolder/README.md index 05a68dafdd..0bfd2f8a35 100644 --- a/plugins/scaffolder/README.md +++ b/plugins/scaffolder/README.md @@ -1,10 +1,86 @@ # Scaffolder Frontend -WORK IN PROGRESS +This is the React frontend for the default Backstage [software +templates](https://backstage.io/docs/features/software-templates/software-templates-index). +This package supplies interfaces related to showing available templates in the +Backstage catalog and the workflow to create software using those templates. -This is the frontend part of the default scaffolder plugin. +## Installation + +This `@backstage/plugin-scaffolder` package comes installed by default in any +Backstage application created with `npx @backstage/create-app`, so installation +is not usually required. + +To check if you already have the package, look under +`packages/app/package.json`, in the `dependencies` block, for +`@backstage/plugin-scaffolder`. The instructions below walk through restoring +the plugin, if you previously removed it. + +### Install the package + +```bash +# From your Backstage root directory +cd packages/app +yarn add @backstage/plugin-scaffolder +``` + +### Add the plugin to your `packages/app` + +Add the root page that the scaffolder plugin provides to your app. You can +choose any path for the route, but we recommend the following: + +```diff +// packages/app/src/App.tsx ++import { ScaffolderPage } from '@backstage/plugin-scaffolder'; + + + + } /> + }> + {entityPage} + ++ } />; + ... + +``` + +The scaffolder plugin also has one external route that needs to be bound for it +to function: the `registerComponent` route which should link to the page where +the user can register existing software component. In a typical setup, the +register component route will be linked to the `catalog-import` plugin's import +page: + +```diff +// packages/app/src/App.tsx ++import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; ++import { catalogImportPlugin } from '@backstage/plugin-catalog-import'; + +const app = createApp({ + // ... + bindRoutes({ bind }) { ++ bind(scaffolderPlugin.externalRoutes, { ++ registerComponent: catalogImportPlugin.routes.importPage, ++ }); + }, +}); +``` + +You may also want to add a link to the scaffolder page to your application +sidebar: + +```diff +// packages/app/src/components/Root/Root.tsx ++import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + ++ ; + ... + +``` ## Links -- [Backend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend) -- [The Backstage homepage](https://backstage.io) +- [scaffolder-backend](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend) + provides the backend API for this frontend. From 21a03156789698687ca37ce03ce2643baed17772 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Jun 2021 04:06:18 +0000 Subject: [PATCH 138/206] chore(deps): bump @typescript-eslint/parser from 4.14.0 to 4.27.0 Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.14.0 to 4.27.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.27.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 81 +++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index f633e410ea..29cfc1616d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -54,7 +54,7 @@ "@types/webpack-env": "^1.15.2", "@types/webpack-node-externals": "^2.5.0", "@typescript-eslint/eslint-plugin": "^v4.26.0", - "@typescript-eslint/parser": "^v4.14.0", + "@typescript-eslint/parser": "^v4.27.0", "@yarnpkg/lockfile": "^1.1.0", "babel-plugin-dynamic-import-node": "^2.3.3", "bfj": "^7.0.2", diff --git a/yarn.lock b/yarn.lock index ab856cd7a0..2991972634 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6824,23 +6824,15 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@^v4.14.0": - version "4.14.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.14.0.tgz#62d4cd2079d5c06683e9bfb200c758f292c4dee7" - integrity sha512-sUDeuCjBU+ZF3Lzw0hphTyScmDDJ5QVkyE21pRoBo8iDl7WBtVFS+WDN3blY1CH3SBt7EmYCw6wfmJjF0l/uYg== +"@typescript-eslint/parser@^v4.27.0": + version "4.27.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.27.0.tgz#85447e573364bce4c46c7f64abaa4985aadf5a94" + integrity sha512-XpbxL+M+gClmJcJ5kHnUpBGmlGdgNvy6cehgR6ufyxkEJMGP25tZKCaKyC0W/JVpuhU3VU1RBn7SYUPKSMqQvQ== dependencies: - "@typescript-eslint/scope-manager" "4.14.0" - "@typescript-eslint/types" "4.14.0" - "@typescript-eslint/typescript-estree" "4.14.0" - debug "^4.1.1" - -"@typescript-eslint/scope-manager@4.14.0": - version "4.14.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.14.0.tgz#55a4743095d684e1f7b7180c4bac2a0a3727f517" - integrity sha512-/J+LlRMdbPh4RdL4hfP1eCwHN5bAhFAGOTsvE6SxsrM/47XQiPSgF5MDgLyp/i9kbZV9Lx80DW0OpPkzL+uf8Q== - dependencies: - "@typescript-eslint/types" "4.14.0" - "@typescript-eslint/visitor-keys" "4.14.0" + "@typescript-eslint/scope-manager" "4.27.0" + "@typescript-eslint/types" "4.27.0" + "@typescript-eslint/typescript-estree" "4.27.0" + debug "^4.3.1" "@typescript-eslint/scope-manager@4.26.0": version "4.26.0" @@ -6850,29 +6842,23 @@ "@typescript-eslint/types" "4.26.0" "@typescript-eslint/visitor-keys" "4.26.0" -"@typescript-eslint/types@4.14.0": - version "4.14.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.14.0.tgz#d8a8202d9b58831d6fd9cee2ba12f8a5a5dd44b6" - integrity sha512-VsQE4VvpldHrTFuVPY1ZnHn/Txw6cZGjL48e+iBxTi2ksa9DmebKjAeFmTVAYoSkTk7gjA7UqJ7pIsyifTsI4A== +"@typescript-eslint/scope-manager@4.27.0": + version "4.27.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.27.0.tgz#b0b1de2b35aaf7f532e89c8e81d0fa298cae327d" + integrity sha512-DY73jK6SEH6UDdzc6maF19AHQJBFVRf6fgAXHPXCGEmpqD4vYgPEzqpFz1lf/daSbOcMpPPj9tyXXDPW2XReAw== + dependencies: + "@typescript-eslint/types" "4.27.0" + "@typescript-eslint/visitor-keys" "4.27.0" "@typescript-eslint/types@4.26.0": version "4.26.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.26.0.tgz#7c6732c0414f0a69595f4f846ebe12616243d546" integrity sha512-rADNgXl1kS/EKnDr3G+m7fB9yeJNnR9kF7xMiXL6mSIWpr3Wg5MhxyfEXy/IlYthsqwBqHOr22boFbf/u6O88A== -"@typescript-eslint/typescript-estree@4.14.0": - version "4.14.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.0.tgz#4bcd67486e9acafc3d0c982b23a9ab8ac8911ed7" - integrity sha512-wRjZ5qLao+bvS2F7pX4qi2oLcOONIB+ru8RGBieDptq/SudYwshveORwCVU4/yMAd4GK7Fsf8Uq1tjV838erag== - dependencies: - "@typescript-eslint/types" "4.14.0" - "@typescript-eslint/visitor-keys" "4.14.0" - debug "^4.1.1" - globby "^11.0.1" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" +"@typescript-eslint/types@4.27.0": + version "4.27.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.27.0.tgz#712b408519ed699baff69086bc59cd2fc13df8d8" + integrity sha512-I4ps3SCPFCKclRcvnsVA/7sWzh7naaM/b4pBO2hVxnM3wrU51Lveybdw5WoIktU/V4KfXrTt94V9b065b/0+wA== "@typescript-eslint/typescript-estree@4.26.0": version "4.26.0" @@ -6887,13 +6873,18 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.14.0": - version "4.14.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.0.tgz#b1090d9d2955b044b2ea2904a22496849acbdf54" - integrity sha512-MeHHzUyRI50DuiPgV9+LxcM52FCJFYjJiWHtXlbyC27b80mfOwKeiKI+MHOTEpcpfmoPFm/vvQS88bYIx6PZTA== +"@typescript-eslint/typescript-estree@4.27.0": + version "4.27.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.27.0.tgz#189a7b9f1d0717d5cccdcc17247692dedf7a09da" + integrity sha512-KH03GUsUj41sRLLEy2JHstnezgpS5VNhrJouRdmh6yNdQ+yl8w5LrSwBkExM+jWwCJa7Ct2c8yl8NdtNRyQO6g== dependencies: - "@typescript-eslint/types" "4.14.0" - eslint-visitor-keys "^2.0.0" + "@typescript-eslint/types" "4.27.0" + "@typescript-eslint/visitor-keys" "4.27.0" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" "@typescript-eslint/visitor-keys@4.26.0": version "4.26.0" @@ -6903,6 +6894,14 @@ "@typescript-eslint/types" "4.26.0" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@4.27.0": + version "4.27.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.27.0.tgz#f56138b993ec822793e7ebcfac6ffdce0a60cb81" + integrity sha512-es0GRYNZp0ieckZ938cEANfEhsfHrzuLrePukLKtY3/KPXcq1Xd555Mno9/GOgXhKzn0QfkDLVgqWO3dGY80bg== + dependencies: + "@typescript-eslint/types" "4.27.0" + eslint-visitor-keys "^2.0.0" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -13991,7 +13990,7 @@ globby@11.0.1: merge2 "^1.3.0" slash "^3.0.0" -globby@11.0.3, globby@^11.0.0, globby@^11.0.1, globby@^11.0.2, globby@^11.0.3: +globby@11.0.3, globby@^11.0.0, globby@^11.0.2, globby@^11.0.3: version "11.0.3" resolved "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== @@ -25530,7 +25529,7 @@ tslib@~2.1.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== -tsutils@^3.17.1, tsutils@^3.21.0: +tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== From 55a0aec9015c8f2fd42e73aafc5e82f9b76317a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Jun 2021 04:17:25 +0000 Subject: [PATCH 139/206] chore(deps-dev): bump @types/js-yaml from 4.0.0 to 4.0.1 Bumps [@types/js-yaml](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/js-yaml) from 4.0.0 to 4.0.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/js-yaml) --- updated-dependencies: - dependency-name: "@types/js-yaml" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ab856cd7a0..d9c74f848d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5930,9 +5930,9 @@ integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== "@types/js-yaml@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.0.tgz#d1a11688112091f2c711674df3a65ea2f47b5dfb" - integrity sha512-4vlpCM5KPCL5CfGmTbpjwVKbISRYhduEJvvUWsH5EB7QInhEj94XPZ3ts/9FPiLZFqYO0xoW4ZL8z2AabTGgJA== + version "4.0.1" + resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.1.tgz#5544730b65a480b18ace6b6ce914e519cec2d43b" + integrity sha512-xdOvNmXmrZqqPy3kuCQ+fz6wA0xU5pji9cd1nDrflWaAWtYLLGk5ykW0H6yg5TVyehHP1pfmuuSaZkhP+kspVA== "@types/jscodeshift@^0.11.0": version "0.11.0" From facbe605322a0797f71da2d6556a894e29404c05 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 16 Jun 2021 15:36:04 +0800 Subject: [PATCH 140/206] fix: results are not accurate for search Signed-off-by: Kevin --- plugins/search-backend-node/src/engines/LunrSearchEngine.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index be51738920..18f2058583 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -90,6 +90,10 @@ export class LunrSearchEngine implements SearchEngine { index(type: string, documents: IndexableDocument[]): void { const lunrBuilder = new lunr.Builder(); + + lunrBuilder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer); + lunrBuilder.searchPipeline.add(lunr.stemmer); + // Make this lunr index aware of all relevant fields. Object.keys(documents[0]).forEach(field => { lunrBuilder.field(field); From 6c12f0ec68a003ea39288d4fb6503091a3b1091f Mon Sep 17 00:00:00 2001 From: Vitor Capretz Date: Sat, 12 Jun 2021 13:44:34 +0200 Subject: [PATCH 141/206] Create changeset Signed-off-by: Vitor Capretz --- .changeset/ninety-horses-rescue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ninety-horses-rescue.md diff --git a/.changeset/ninety-horses-rescue.md b/.changeset/ninety-horses-rescue.md new file mode 100644 index 0000000000..b3e0decdde --- /dev/null +++ b/.changeset/ninety-horses-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sentry': patch +--- + +Migrated the package from `timeago.js` to `luxon`. See #4278 From 20d9c7d3849377b0339c8e9e0ab12f4bcd07f9f1 Mon Sep 17 00:00:00 2001 From: Tejas Kumar Date: Wed, 16 Jun 2021 10:08:06 +0200 Subject: [PATCH 142/206] Address review comments Signed-off-by: Tejas Kumar --- .../techdocs-backend/src/DocsBuilder/builder.ts | 15 ++++++++------- plugins/techdocs-backend/src/service/router.ts | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index e43ed72c31..dce391ff3f 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getRootLogger, loadBackendConfig } from '@backstage/backend-common'; import { Entity, ENTITY_DEFAULT_NAMESPACE, @@ -33,6 +32,7 @@ import fs from 'fs-extra'; import os from 'os'; import path from 'path'; import { Logger } from 'winston'; +import { Config } from '@backstage/config'; import { BuildMetadataStorage } from './BuildMetadataStorage'; type DocsBuilderArguments = { @@ -41,6 +41,7 @@ type DocsBuilderArguments = { publisher: PublisherBase; entity: Entity; logger: Logger; + config: Config; }; export class DocsBuilder { @@ -49,6 +50,7 @@ export class DocsBuilder { private publisher: PublisherBase; private entity: Entity; private logger: Logger; + private config: Config; constructor({ preparers, @@ -56,12 +58,14 @@ export class DocsBuilder { publisher, entity, logger, + config, }: DocsBuilderArguments) { this.preparer = preparers.get(entity); this.generator = generators.get(entity); this.publisher = publisher; this.entity = entity; this.logger = logger; + this.config = config; } public async build(): Promise { @@ -142,12 +146,9 @@ export class DocsBuilder { )}`, ); - // Create a temporary directory to store the generated files in. - const config = await loadBackendConfig({ - argv: process.argv, - logger: getRootLogger(), - }); - const workingDir = config.getOptionalString('backend.workingDirectory'); + const workingDir = this.config.getOptionalString( + 'backend.workingDirectory', + ); const tmpdirPath = workingDir || os.tmpdir(); // Fixes a problem with macOS returning a path that is a symlink const tmpdirResolvedPath = fs.realpathSync(tmpdirPath); diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 9f383ea31d..1cd4075da0 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -164,6 +164,7 @@ export async function createRouter({ publisher, logger, entity, + config, }); let foundDocs = false; switch (publisherType) { From 8adb6f6bcd0d20a0aa421af8a5b3b9d66f9f6e4d Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 16 Jun 2021 10:34:48 +0200 Subject: [PATCH 143/206] feat: enable backwards compatability and write a simple test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Co-authored-by: Patrik Oldsberg Signed-off-by: blam --- .../src/providers/google/provider.test.ts | 89 +++++++++++++++++++ .../src/providers/google/provider.ts | 86 +++++++++++++----- plugins/auth-backend/src/providers/types.ts | 1 + 3 files changed, 156 insertions(+), 20 deletions(-) create mode 100644 plugins/auth-backend/src/providers/google/provider.test.ts diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts new file mode 100644 index 0000000000..a8b5b92b5e --- /dev/null +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -0,0 +1,89 @@ +/* + * 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 } from './provider'; +import * as helpers from '../../lib/passport/PassportStrategyHelper'; +import { OAuthResult } from '../../lib/oauth'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TokenIssuer } from '../../identity/types'; +import { CatalogIdentityClient } from '../../lib/catalog'; + +const mockFrameHandler = (jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown) as jest.MockedFunction< + () => Promise<{ result: OAuthResult; privateInfo: any }> +>; + +describe('createGoogleProvider', () => { + it('should auth', async () => { + const tokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + }; + const catalogIdentityClient = { + findUser: jest.fn(), + }; + + const provider = new GoogleAuthProvider({ + logger: getVoidLogger(), + catalogIdentityClient: (catalogIdentityClient as unknown) as CatalogIdentityClient, + tokenIssuer: (tokenIssuer as unknown) as TokenIssuer, + profileTransform: async ({ fullProfile }) => ({ + email: fullProfile.emails![0]!.value, + displayName: fullProfile.displayName, + picture: 'http://google.com/lols', + }), + clientId: 'mock', + clientSecret: 'mock', + callbackUrl: 'mock', + }); + + mockFrameHandler.mockResolvedValueOnce({ + result: { + fullProfile: { + emails: [{ value: 'conrad@example.com' }], + displayName: 'Conrad', + id: 'conrad', + provider: 'google', + }, + params: { + id_token: 'idToken', + scope: 'scope', + expires_in: 123, + }, + accessToken: 'accessToken', + }, + privateInfo: { + refreshToken: 'wacka', + }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual({ + providerInfo: { + accessToken: 'accessToken', + expiresInSeconds: 123, + idToken: 'idToken', + scope: 'scope', + }, + profile: { + email: 'conrad@example.com', + displayName: 'Conrad', + picture: 'http://google.com/lols', + }, + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index b97e625699..f2feb7155b 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -44,6 +44,7 @@ import { RedirectInfo, SignInResolver, } from '../types'; +import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -54,6 +55,7 @@ type Options = OAuthProviderOptions & { profileTransform: ProfileTransform; tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }; export class GoogleAuthProvider implements OAuthHandlers { @@ -62,12 +64,14 @@ export class GoogleAuthProvider implements OAuthHandlers { private readonly profileTransform: ProfileTransform; private readonly tokenIssuer: TokenIssuer; private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; constructor(options: Options) { this.signInResolver = options.signInResolver; this.profileTransform = options.profileTransform; this.tokenIssuer = options.tokenIssuer; this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; this._strategy = new GoogleStrategy( { clientID: options.clientId, @@ -163,6 +167,7 @@ export class GoogleAuthProvider implements OAuthHandlers { { tokenIssuer: this.tokenIssuer, catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, }, ); } @@ -171,7 +176,10 @@ export class GoogleAuthProvider implements OAuthHandlers { } } -const emailSignInResolver: SignInResolver = async (info, ctx) => { +export const googleEmailSignInResolver: SignInResolver = async ( + info, + ctx, +) => { const { profile } = info; if (!profile.email) { @@ -190,6 +198,38 @@ const emailSignInResolver: SignInResolver = async (info, ctx) => { return { id: entity.metadata.name, entity, token }; }; +export const googleDefaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Google profile contained no email'); + } + + let userId: string; + try { + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'google.com/email': profile.email, + }, + }); + userId = entity.metadata.name; + } catch (error) { + ctx.logger.warn( + `Failed to look up user, ${error}, falling back to allowing login based on email pattern, this will probably break in the future`, + ); + userId = profile.email.split('@')[0]; + } + + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: userId, ent: [`user:default/${userId}`] }, + }); + + return { id: userId, token }; +}; + export type GoogleProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -200,21 +240,28 @@ export type GoogleProviderOptions = { /** * Configure sign-in for this provider, without it the provider can not be used to sign users in. */ + /** + * Maps an auth result to a Backstage identity for the user. + * + * Set to `'email'` to use the default email-based sign in resolver, which will search + * the catalog for a single user entity that has a matching `google.com/email` annotation. + */ signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - * - * Set to `'email'` to use the default email-based sign in resolver, which will search - * the catalog for a single user entity that has a matching `google.com/email` annotation. - */ - resolver?: 'email' | SignInResolver; + resolver?: SignInResolver; }; }; export const createGoogleProvider = ( options?: GoogleProviderOptions, ): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer, catalogApi }) => + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -233,17 +280,15 @@ export const createGoogleProvider = ( profileTransform = options.profileTransform; } - let signInResolver: SignInResolver | undefined = undefined; - const resolver = options?.signIn?.resolver; - if (resolver === 'email') { - signInResolver = emailSignInResolver; - } else if (typeof resolver === 'function') { - signInResolver = info => - resolver(info, { - catalogIdentityClient, - tokenIssuer, - }); - } + const signInResolverFn = + options?.signIn?.resolver ?? googleDefaultSignInResolver; + + const signInResolver: SignInResolver = info => + signInResolverFn(info, { + catalogIdentityClient, + tokenIssuer, + logger, + }); const provider = new GoogleAuthProvider({ clientId, @@ -253,6 +298,7 @@ export const createGoogleProvider = ( profileTransform, tokenIssuer, catalogIdentityClient, + logger, }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 1f71bfe59c..305ff7e776 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -215,6 +215,7 @@ export type SignInResolver = ( context: { tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }, ) => Promise; From 2fbded87e5dca60c96b46157270b9e490b56eaa5 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 16 Jun 2021 10:38:26 +0200 Subject: [PATCH 144/206] chore: revert the changes in app/backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Co-authored-by: Patrik Oldsberg Signed-off-by: blam --- .changeset/seven-adults-act.md | 18 +++++++++++++---- packages/backend/src/plugins/auth.ts | 29 ++-------------------------- test.yaml | 0 3 files changed, 16 insertions(+), 31 deletions(-) create mode 100644 test.yaml diff --git a/.changeset/seven-adults-act.md b/.changeset/seven-adults-act.md index 93541e06a6..fa2839aad2 100644 --- a/.changeset/seven-adults-act.md +++ b/.changeset/seven-adults-act.md @@ -2,14 +2,24 @@ '@backstage/plugin-auth-backend': patch --- -Adds custom sign-in resolvers and profile transformation for Google auth provider. Read more about what this means for Backstage user identity and determining ownership of entities https://backstage.io/docs/auth/identity-resolver -Related the [RFC] From Identity to Ownership, v2 https://github.com/backstage/backstage/issues/4089 +Adds support for custom sign-in resolvers and profile transformations for the +Google auth provider. -Adds `ent` field in the claims of Backstage ID Token with a list of entity references containing identity and membership info about the user across multiple systems. +Adds an `ent` claim in Backstage tokens, with a list of +[entity references](https://backstage.io/docs/features/software-catalog/references) +related to your signed-in user's identities and groups across multiple systems. -Adds an optional `providerFactories` to the `createRouter` exported by the auth-backend plugin. +Adds an optional `providerFactories` argument to the `createRouter` exported by +the `auth-backend` plugin. Updates `BackstageIdentity` so that - `idToken` is deprecated in favor of `token` - An optional `entity` field is added which represents the entity that the user is represented by within Backstage. + +More information: + +- [The identity resolver documentation](https://backstage.io/docs/auth/identity-resolver) + explains the concepts and shows how to implement your own. +- The [From Identity to Ownership](https://github.com/backstage/backstage/issues/4089) + RFC contains details about how this affects ownership in the catalog diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 3157284df7..2b1c85f052 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - createGoogleProvider, - createRouter, -} from '@backstage/plugin-auth-backend'; +import { createRouter } from '@backstage/plugin-auth-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -27,27 +24,5 @@ export default async function createPlugin({ config, discovery, }: PluginEnvironment): Promise { - return await createRouter({ - logger, - config, - database, - discovery, - providerFactories: { - google: createGoogleProvider({ - signIn: { - // resolver: 'email', - resolver: async ({ profile: { email } }, ctx) => { - if (!email) { - throw new Error('No email associated with user account'); - } - const id = email.split('@')[0]; - const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id, ent: [`User:default/${id}`] }, - }); - return { id, token }; - }, - }, - }), - }, - }); + return await createRouter({ logger, config, database, discovery }); } diff --git a/test.yaml b/test.yaml new file mode 100644 index 0000000000..e69de29bb2 From 551c050e2310c259eb8173a0f87ca9e2ce980f8a Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 16 Jun 2021 10:42:48 +0200 Subject: [PATCH 145/206] feat: actually export the googleSignInResovlers for use in apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Co-authored-by: Patrik Oldsberg Signed-off-by: blam --- plugins/auth-backend/src/providers/google/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index 2615a2d8e5..8bff8b250b 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -14,5 +14,9 @@ * limitations under the License. */ -export { createGoogleProvider } from './provider'; +export { + createGoogleProvider, + googleDefaultSignInResolver, + googleEmailSignInResolver, +} from './provider'; export type { GoogleProviderOptions } from './provider'; From f5f290f1c26ae9d152517a427c9a85020ec9229c Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 16 Jun 2021 10:56:42 +0200 Subject: [PATCH 146/206] feat: update the public api instead of profileTransform it is AuthHandler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Co-authored-by: Patrik Oldsberg Signed-off-by: blam --- .../src/providers/google/provider.test.ts | 10 ++++--- .../src/providers/google/provider.ts | 26 +++++++++---------- plugins/auth-backend/src/providers/types.ts | 10 ++++--- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index a8b5b92b5e..45df5bd319 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -42,10 +42,12 @@ describe('createGoogleProvider', () => { logger: getVoidLogger(), catalogIdentityClient: (catalogIdentityClient as unknown) as CatalogIdentityClient, tokenIssuer: (tokenIssuer as unknown) as TokenIssuer, - profileTransform: async ({ fullProfile }) => ({ - email: fullProfile.emails![0]!.value, - displayName: fullProfile.displayName, - picture: 'http://google.com/lols', + authHandler: async ({ fullProfile }) => ({ + profile: { + email: fullProfile.emails![0]!.value, + displayName: fullProfile.displayName, + picture: 'http://google.com/lols', + }, }), clientId: 'mock', clientSecret: 'mock', diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index f2feb7155b..ace0c8e132 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -40,7 +40,7 @@ import { } from '../../lib/passport'; import { AuthProviderFactory, - ProfileTransform, + AuthHandler, RedirectInfo, SignInResolver, } from '../types'; @@ -52,7 +52,7 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; - profileTransform: ProfileTransform; + authHandler: AuthHandler; tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; logger: Logger; @@ -61,14 +61,14 @@ type Options = OAuthProviderOptions & { export class GoogleAuthProvider implements OAuthHandlers { private readonly _strategy: GoogleStrategy; private readonly signInResolver?: SignInResolver; - private readonly profileTransform: ProfileTransform; + private readonly authHandler: AuthHandler; private readonly tokenIssuer: TokenIssuer; private readonly catalogIdentityClient: CatalogIdentityClient; private readonly logger: Logger; constructor(options: Options) { this.signInResolver = options.signInResolver; - this.profileTransform = options.profileTransform; + this.authHandler = options.authHandler; this.tokenIssuer = options.tokenIssuer; this.catalogIdentityClient = options.catalogIdentityClient; this.logger = options.logger; @@ -146,7 +146,7 @@ export class GoogleAuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const profile = await this.profileTransform(result); + const { profile } = await this.authHandler(result); const response: OAuthResponse = { providerInfo: { @@ -235,7 +235,7 @@ export type GoogleProviderOptions = { * The profile transformation function used to verify and convert the auth response * into the profile that will be presented to the user. */ - profileTransform?: ProfileTransform; + authHandler?: AuthHandler; /** * Configure sign-in for this provider, without it the provider can not be used to sign users in. @@ -272,13 +272,11 @@ export const createGoogleProvider = ( tokenIssuer, }); - let profileTransform: ProfileTransform = async ({ - fullProfile, - params, - }) => makeProfileInfo(fullProfile, params.id_token); - if (options?.profileTransform) { - profileTransform = options.profileTransform; - } + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); const signInResolverFn = options?.signIn?.resolver ?? googleDefaultSignInResolver; @@ -295,7 +293,7 @@ export const createGoogleProvider = ( clientSecret, callbackUrl, signInResolver, - profileTransform, + authHandler, tokenIssuer, catalogIdentityClient, logger, diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 305ff7e776..e4bbaa091c 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -219,14 +219,16 @@ export type SignInResolver = ( }, ) => Promise; +export type AuthHandlerResult = { profile: ProfileInfo }; + /** - * A transformation function called every time the user authenticates using the provider. + * The AuthHandler function is called every time the user authenticates using the provider. * - * The transform should return a profile that represents the session for the user in the frontend. + * The handler should return a profile that represents the session for the user in the frontend. * * Throwing an error in the function will cause the authentication to fail, making it * possible to use this function as a way to limit access to a certain group of users. */ -export type ProfileTransform = ( +export type AuthHandler = ( input: AuthResult, -) => Promise; +) => Promise; From ca60400ebeba1604c88affe5f290836ae4cb0c7a Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 16 Jun 2021 11:03:57 +0200 Subject: [PATCH 147/206] chore: update documentation a little bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: blam Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Co-authored-by: Patrik Oldsberg --- docs/auth/identity-resolver.md | 35 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 9414c9aa14..8c1aefb4d3 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -108,6 +108,8 @@ It can be enabled like this ```tsx # File: packages/backend/src/plugins/auth.ts ... +import { googleEmailSignInResolver } from '@backstage/plugin-auth-backend'; + export default async function createPlugin({ ... }: PluginEnvironment): Promise { @@ -116,21 +118,26 @@ export default async function createPlugin({ providerFactories: { google: createGoogleProvider({ signIn: { - resolver: 'email' + resolver: googleEmailSignInResolver } ... ``` -## Profile transform +## AuthHandler -Similar to a custom sign-in resolver, you can also write a custom profile -transformation function which is used to verify and convert the auth response -into the profile that will be presented to the user. This is where you can -customize things like display name and profile picture. +Similar to a custom sign-in resolver, you can also write a custom auth handler +function which is used to verify and convert the auth response into the profile +that will be presented to the user. This is where you can customize things like +display name and profile picture. + +This is also the place where you can do authorization and validation of the user +and throw errors if the user should not be allowed access in Backstage. ```tsx # File: packages/backend/src/plugins/auth.ts ... +import { googleEmailSignInResolver } from '@backstage/plugin-auth-backend'; + export default async function createPlugin({ ... }: PluginEnvironment): Promise { @@ -139,17 +146,19 @@ export default async function createPlugin({ providerFactories: { google: createGoogleProvider({ signIn: { - resolver: 'email' + resolver: googleEmailSignInResolver }, - profileTransform: async ({ + authHandler: async ({ fullProfile // Type: passport.Profile, idToken // Type: (Optional) string, - }): ProfileInfo => { - // Do stuff + }) => { + // Custom validation return { - email, - picture, - displayName, + profile: { + email, + picture, + displayName, + } }; } }) From 14aad6113c3254b3131315d3d64563c15e2e1d4b Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 16 Jun 2021 17:26:21 +0800 Subject: [PATCH 148/206] fix: results are not accurate for search Signed-off-by: Kevin --- .changeset/cyan-drinks-dream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cyan-drinks-dream.md diff --git a/.changeset/cyan-drinks-dream.md b/.changeset/cyan-drinks-dream.md new file mode 100644 index 0000000000..acf8ec37e2 --- /dev/null +++ b/.changeset/cyan-drinks-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': minor +--- + +Searching for things like "World" in "Hello World." returns no results. From eb93bf2720c5d14a70d270320a1ecfc95e89f4aa Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 16 Jun 2021 17:28:42 +0800 Subject: [PATCH 149/206] fix: results are not accurate for search Signed-off-by: Kevin --- .changeset/cyan-drinks-dream.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cyan-drinks-dream.md b/.changeset/cyan-drinks-dream.md index acf8ec37e2..966de7226a 100644 --- a/.changeset/cyan-drinks-dream.md +++ b/.changeset/cyan-drinks-dream.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-search-backend-node': minor +'@backstage/plugin-search-backend-node': patch --- Searching for things like "World" in "Hello World." returns no results. From cb09e445ea73236325f8599b980dcc0c7ba1aaf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 16 Jun 2021 11:30:15 +0200 Subject: [PATCH 150/206] Implement `NextCatalogBuilder.addEntityProvider` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/nice-dryers-dream.md | 5 +++ .../src/next/NextCatalogBuilder.ts | 32 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 .changeset/nice-dryers-dream.md diff --git a/.changeset/nice-dryers-dream.md b/.changeset/nice-dryers-dream.md new file mode 100644 index 0000000000..a447bb564a --- /dev/null +++ b/.changeset/nice-dryers-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Implement `NextCatalogBuilder.addEntityProvider` diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index e859a225e5..2551c16879 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -63,7 +63,11 @@ import { } from '../ingestion/processors/PlaceholderProcessor'; import { defaultEntityDataParser } from '../ingestion/processors/util/parse'; import { LocationAnalyzer } from '../ingestion/types'; -import { CatalogProcessingEngine, LocationService } from '../next/types'; +import { + CatalogProcessingEngine, + EntityProvider, + LocationService, +} from '../next/types'; import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider'; import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase'; import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine'; @@ -105,6 +109,7 @@ export class NextCatalogBuilder { private entityPoliciesReplace: boolean; private placeholderResolvers: Record; private fieldFormatValidators: Partial; + private entityProviders: EntityProvider[]; private processors: CatalogProcessor[]; private processorsReplace: boolean; private parser: CatalogProcessorParser | undefined; @@ -116,6 +121,7 @@ export class NextCatalogBuilder { this.entityPoliciesReplace = false; this.placeholderResolvers = {}; this.fieldFormatValidators = {}; + this.entityProviders = []; this.processors = []; this.processorsReplace = false; this.parser = undefined; @@ -198,6 +204,20 @@ export class NextCatalogBuilder { return this; } + /** + * Adds or replaces entity providers. These are responsible for bootstrapping + * the list of entities out of original data sources. For example, there is + * one entity source for the config locations, and one for the database + * stored locations. If you ingest entities out of a third party system, you + * may want to implement that in terms of an entity provider as well. + * + * @param providers One or more entity providers + */ + addEntityProvider(...providers: EntityProvider[]): NextCatalogBuilder { + this.entityProviders.push(...providers); + return this; + } + /** * Adds entity processors. These are responsible for reading, parsing, and * processing entities before they are persisted in the catalog. @@ -277,13 +297,18 @@ export class NextCatalogBuilder { policy, }); const entitiesCatalog = new NextEntitiesCatalog(dbClient); + const stitcher = new Stitcher(dbClient, logger); const locationStore = new DefaultLocationStore(dbClient); - const stitcher = new Stitcher(dbClient, logger); const configLocationProvider = new ConfigLocationEntityProvider(config); + const entityProviders = lodash.uniqBy( + [...this.entityProviders, locationStore, configLocationProvider], + provider => provider.getProviderName(), + ); + const processingEngine = new DefaultCatalogProcessingEngine( logger, - [locationStore, configLocationProvider], + entityProviders, processingDatabase, orchestrator, stitcher, @@ -295,6 +320,7 @@ export class NextCatalogBuilder { locationStore, orchestrator, ); + return { entitiesCatalog, locationsCatalog, From 8061d1eb9e0885b01aa743c9bb98fdcf3461d2f0 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 16 Jun 2021 11:40:24 +0200 Subject: [PATCH 151/206] docs: rework the documentation to make the example a little simpler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: blam Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Co-authored-by: Patrik Oldsberg --- docs/auth/identity-resolver.md | 38 +++++++++++++--------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 8c1aefb4d3..28bcd7c211 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -36,35 +36,25 @@ export default async function createPlugin({ google: createGoogleProvider({ signIn: { resolver: async ({ profile: { email } }, ctx) => { - if (!email) { - throw new Error('No email associated with user account'); - } + // Call a custom validator function that checks that the email is + // valid and on our own company's domain, and throws an Error if it + // isn't + validateEmail(email); - // Ignore email addresses which do not belong to company's domain name - if (email.split('@')[1] !== 'mycompany.com') { - throw new Error('Unrecognized domain name of the email ID used to sign in.') - } - - // List of entity references that denote the identity and membership of the user + // List of entity references that denote the identity and + // membership of the user const ent = []; - // Let's use the username in the email ID as the user's default unique identifier inside Backstage - const id = email.split('@')[0]; - // Let's add the unique ID in the list + // Let's use the username in the email ID as the user's default + // unique identifier inside Backstage + const [id] = email.split('@'); + + // Add the unique ID in the list ent.push(`User:default/${id}`) - // Let's call the GitHub Enterprise API inside the company and get the teams that the user belongs to - const gheUsername = getGheUsername(email); - const gheTeams = getGheTeams(gheUsername); - - // Let's add the GHE identities to ent claims inside a new ghe namespace to keep things separate from the - // default namespace. - ent.push(`User:ghe/${gheUsername}`) - gheTeams.forEach(team => ent.push(`Group:ghe/${team}`)) - // Let's call the internal LDAP provider to get a list of groups the user belongs to - const ldapGroups = getLdapGroups(email); - ldapGroups.forEach(ldapGroup => ent.push(`Group:myldap/${ldapGroup}`)) + const ldapGroups = await getLdapGroups(email); + ldapGroups.forEach(ldapGroup => ent.push(`Group:default/${ldapGroup}`)) // Issue the token containing the entity claims const token = await ctx.tokenIssuer.issueToken({ @@ -152,7 +142,7 @@ export default async function createPlugin({ fullProfile // Type: passport.Profile, idToken // Type: (Optional) string, }) => { - // Custom validation + // Custom validation code goes here return { profile: { email, From 1aa31f0afcb8bbf0603416c703b7f40d234ddedb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 16 Jun 2021 12:13:27 +0200 Subject: [PATCH 152/206] auth-backend: add support for GitLab auth refresh Signed-off-by: Patrik Oldsberg --- .changeset/violet-ads-change.md | 5 + .../src/providers/gitlab/provider.test.ts | 78 +++++++------ .../src/providers/gitlab/provider.ts | 110 +++++++++++++----- 3 files changed, 129 insertions(+), 64 deletions(-) create mode 100644 .changeset/violet-ads-change.md diff --git a/.changeset/violet-ads-change.md b/.changeset/violet-ads-change.md new file mode 100644 index 0000000000..1de5813982 --- /dev/null +++ b/.changeset/violet-ads-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add support for refreshing GitLab auth sessions. diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index 82588250cf..db941f0213 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -27,24 +27,29 @@ describe('GitlabAuthProvider', () => { it('should transform to type OAuthResponse', async () => { const tests = [ { - result: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - fullProfile: { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'gitlab', - displayName: 'Jimmy Markum', - emails: [ - { - value: 'jimmymarkum@gmail.com', - }, - ], - avatarUrl: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', + input: { + result: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + fullProfile: { + id: 'uid-123', + username: 'jimmymarkum', + provider: 'gitlab', + displayName: 'Jimmy Markum', + emails: [ + { + value: 'jimmymarkum@gmail.com', + }, + ], + avatarUrl: + 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', + }, + params: { + scope: 'user_read write_repository', + expires_in: 100, + }, }, - params: { - scope: 'user_read write_repository', - expires_in: 100, + privateInfo: { + refreshToken: 'gacn9jdgm19me19xasczxcm9n7', }, }, expect: { @@ -65,23 +70,28 @@ describe('GitlabAuthProvider', () => { }, }, { - result: { - accessToken: - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - fullProfile: { - id: 'ipd12039', - username: 'daveboyle', - provider: 'gitlab', - displayName: 'Dave Boyle', - emails: [ - { - value: 'daveboyle@gitlab.org', - }, - ], + input: { + result: { + accessToken: + 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', + fullProfile: { + id: 'ipd12039', + username: 'daveboyle', + provider: 'gitlab', + displayName: 'Dave Boyle', + emails: [ + { + value: 'daveboyle@gitlab.org', + }, + ], + }, + params: { + scope: 'read_repository', + expires_in: 200, + }, }, - params: { - scope: 'read_repository', - expires_in: 200, + privateInfo: { + refreshToken: 'gacn96f3y6y5jdgm19mec348nqrty719xasczf356yxcm9n7', }, }, expect: { @@ -109,7 +119,7 @@ describe('GitlabAuthProvider', () => { baseUrl: 'mock', }); for (const test of tests) { - mockFrameHandler.mockResolvedValueOnce({ result: test.result }); + mockFrameHandler.mockResolvedValueOnce(test.input); const { response } = await provider.handler({} as any); expect(response).toEqual(test.expect); } diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index b8de398af5..3749bf7134 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -17,8 +17,10 @@ import express from 'express'; import { Strategy as GitlabStrategy } from 'passport-gitlab2'; import { - executeFrameHandlerStrategy, executeRedirectStrategy, + executeFrameHandlerStrategy, + executeRefreshTokenStrategy, + executeFetchUserProfileStrategy, makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; @@ -30,14 +32,40 @@ import { OAuthResponse, OAuthEnvironmentHandler, OAuthStartRequest, + OAuthRefreshRequest, encodeState, OAuthResult, } from '../../lib/oauth'; +type FullProfile = OAuthResult['fullProfile'] & { + avatarUrl?: string; +}; + +type PrivateInfo = { + refreshToken: string; +}; + export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; }; +function transformProfile(fullProfile: FullProfile) { + const profile = makeProfileInfo({ + ...fullProfile, + photos: [ + ...(fullProfile.photos ?? []), + ...(fullProfile.avatarUrl ? [{ value: fullProfile.avatarUrl }] : []), + ], + }); + + let id = fullProfile.id; + if (profile.email) { + id = profile.email.split('@')[0]; + } + + return { id, profile }; +} + export class GitlabAuthProvider implements OAuthHandlers { private readonly _strategy: GitlabStrategy; @@ -51,12 +79,18 @@ export class GitlabAuthProvider implements OAuthHandlers { }, ( accessToken: any, - _refreshToken: any, + refreshToken: any, params: any, fullProfile: any, - done: PassportDoneCallback, + done: PassportDoneCallback, ) => { - done(undefined, { fullProfile, params, accessToken }); + done( + undefined, + { fullProfile, params, accessToken }, + { + refreshToken, + }, + ); }, ); } @@ -68,33 +102,16 @@ export class GitlabAuthProvider implements OAuthHandlers { }); } - async handler(req: express.Request): Promise<{ response: OAuthResponse }> { - const { result } = await executeFrameHandlerStrategy( - req, - this._strategy, - ); + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, + PrivateInfo + >(req, this._strategy); const { accessToken, params } = result; - const fullProfile = result.fullProfile as OAuthResult['fullProfile'] & { - avatarUrl?: string; - }; - const profile = makeProfileInfo( - { - ...fullProfile, - photos: [ - ...(fullProfile.photos ?? []), - ...(fullProfile.avatarUrl ? [{ value: fullProfile.avatarUrl }] : []), - ], - }, - params.id_token, - ); - - // gitlab provides an id numeric value (123) - // as a fallback - let id = fullProfile.id; - if (profile.email) { - id = profile.email.split('@')[0]; - } + const { id, profile } = transformProfile(result.fullProfile); return { response: { @@ -109,6 +126,39 @@ export class GitlabAuthProvider implements OAuthHandlers { id, }, }, + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh(req: OAuthRefreshRequest): Promise { + const { + accessToken, + refreshToken: newRefreshToken, + params, + } = await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); + + const fullProfile = await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + ); + const { id, profile } = transformProfile(fullProfile); + + return { + profile, + providerInfo: { + accessToken, + refreshToken: newRefreshToken, // GitLab expires the old refresh token when used + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }, + backstageIdentity: { + id, + }, }; } } @@ -134,7 +184,7 @@ export const createGitlabProvider = ( }); return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, + disableRefresh: false, providerId, tokenIssuer, }); From e2d68f1ce32441c6b584c82701b98dd3d15d2caa Mon Sep 17 00:00:00 2001 From: Louis Bichard Date: Tue, 8 Jun 2021 18:39:40 +0100 Subject: [PATCH 153/206] fix: truncate long system names Signed-off-by: Louis Bichard --- .changeset/flat-chefs-push.md | 5 ++ .../SystemDiagramCard.test.tsx | 85 ++++++++++++++++--- .../SystemDiagramCard/SystemDiagramCard.tsx | 7 +- 3 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 .changeset/flat-chefs-push.md diff --git a/.changeset/flat-chefs-push.md b/.changeset/flat-chefs-push.md new file mode 100644 index 0000000000..98ee448dba --- /dev/null +++ b/.changeset/flat-chefs-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Truncate long entity names on the system diagram diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx index 337e9d12b6..412cc56309 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx @@ -48,8 +48,8 @@ describe('', () => { apiVersion: 'v1', kind: 'System', metadata: { - name: 'my-system2', - namespace: 'my-namespace2', + name: 'system2', + namespace: 'namespace2', }, relations: [], }; @@ -68,8 +68,8 @@ describe('', () => { ); expect(queryByText(/System Diagram/)).toBeInTheDocument(); - expect(queryByText(/my-namespace2\/my-system2/)).toBeInTheDocument(); - expect(queryByText(/my-namespace\/my-entity/)).not.toBeInTheDocument(); + expect(queryByText(/namespace2\/system2/)).toBeInTheDocument(); + expect(queryByText(/namespace\/entity/)).not.toBeInTheDocument(); }); it('shows related systems', async () => { @@ -81,13 +81,13 @@ describe('', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { - name: 'my-entity', - namespace: 'my-namespace', + name: 'entity', + namespace: 'namespace', }, spec: { owner: 'not-tools@example.com', type: 'service', - system: 'my-system', + system: 'system', }, }, ] as Entity[], @@ -98,15 +98,15 @@ describe('', () => { apiVersion: 'v1', kind: 'System', metadata: { - name: 'my-system', - namespace: 'my-namespace', + name: 'system', + namespace: 'namespace', }, relations: [ { target: { kind: 'Domain', - namespace: 'my-namespace', - name: 'my-domain', + namespace: 'namespace', + name: 'domain', }, type: RELATION_PART_OF, }, @@ -127,7 +127,66 @@ describe('', () => { ); expect(getByText('System Diagram')).toBeInTheDocument(); - expect(getByText('my-namespace/my-system')).toBeInTheDocument(); - expect(getByText('my-namespace/my-entity')).toBeInTheDocument(); + expect(getByText('namespace/system')).toBeInTheDocument(); + expect(getByText('namespace/entity')).toBeInTheDocument(); + }); + + it('should truncate long domains, systems or entities', async () => { + const catalogApi: Partial = { + getEntities: () => + Promise.resolve({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'alongentitythatshouldgettruncated', + namespace: 'namespace', + }, + spec: { + owner: 'not-tools@example.com', + type: 'service', + system: 'system', + }, + }, + ] as Entity[], + }), + }; + + const entity: Entity = { + apiVersion: 'v1', + kind: 'System', + metadata: { + name: 'alongsystemthatshouldgettruncated', + namespace: 'namespace', + }, + relations: [ + { + target: { + kind: 'Domain', + namespace: 'namespace', + name: 'alongdomainthatshouldgettruncated', + }, + type: RELATION_PART_OF, + }, + ], + }; + + const { getByText } = await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(getByText('namespace/alongdomai...')).toBeInTheDocument(); + expect(getByText('namespace/alongsyste...')).toBeInTheDocument(); + expect(getByText('namespace/alongentit...')).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx index 5fd32a6298..fab8163317 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx @@ -89,6 +89,11 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps) { const catalogEntityRoute = useRouteRef(entityRouteRef); const kind = props.node.kind || 'Component'; const ref = parseEntityRef(props.node.id); + const MAX_NAME_LENGTH = 20; + const truncatedNodeName = + props.node.name.length < MAX_NAME_LENGTH + ? props.node.name + : `${props.node.name.slice(0, MAX_NAME_LENGTH)}...`; let nodeClass = classes.componentNode; switch (kind) { @@ -128,7 +133,7 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps) { alignmentBaseline="baseline" style={{ fontWeight: 'bold' }} > - {props.node.name} + {truncatedNodeName} From 878c1851d49784975592482fcb0e1679498cbd9b Mon Sep 17 00:00:00 2001 From: Crevil Date: Wed, 16 Jun 2021 13:31:00 +0200 Subject: [PATCH 154/206] Add topics input to publish:github action This change adds an array input of topics to attach on a repository upon creation. Signed-off-by: Crevil --- .changeset/proud-jars-look.md | 5 +++ .../__mocks__/@octokit/rest/index.ts | 1 + .../actions/builtin/publish/github.test.ts | 33 +++++++++++++++++++ .../actions/builtin/publish/github.ts | 22 +++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 .changeset/proud-jars-look.md diff --git a/.changeset/proud-jars-look.md b/.changeset/proud-jars-look.md new file mode 100644 index 0000000000..0311452b4f --- /dev/null +++ b/.changeset/proud-jars-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Add a `topics` input to `publish:github` action that can be used to set topics on the repository upon creation. diff --git a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts index e0e9efa479..1f27745c0c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts @@ -19,6 +19,7 @@ export const mockGithubClient = { createInOrg: jest.fn(), createForAuthenticatedUser: jest.fn(), addCollaborator: jest.fn(), + replaceAllTopics: jest.fn(), }, users: { getByUsername: jest.fn(), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index dc2f602778..c59ba18bea 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -341,6 +341,39 @@ describe('publish:github', () => { ]); }); + it('should add topics when provided', async () => { + mockGithubClient.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + mockGithubClient.repos.replaceAllTopics.mockResolvedValue({ + data: { + names: ['node.js'], + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + topics: ['node.js'], + }, + }); + + expect(mockGithubClient.repos.replaceAllTopics).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + names: ['node.js'], + }); + }); + it('should call output with the remoteUrl and the repoContentsUrl', async () => { mockGithubClient.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index a3befa0dfe..d7beabe0db 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -48,6 +48,7 @@ export function createPublishGithubAction(options: { sourcePath?: string; repoVisibility: 'private' | 'internal' | 'public'; collaborators: Collaborator[]; + topics?: string[]; }>({ id: 'publish:github', description: @@ -99,6 +100,14 @@ export function createPublishGithubAction(options: { }, }, }, + topics: { + title: 'Topics', + description: 'Uppercase letters no allowed', + type: 'array', + items: { + type: 'string', + }, + }, }, }, output: { @@ -122,6 +131,7 @@ export function createPublishGithubAction(options: { access, repoVisibility = 'private', collaborators, + topics, } = ctx.input; const { owner, repo, host } = parseRepoUrl(repoUrl); @@ -215,6 +225,18 @@ export function createPublishGithubAction(options: { } } + if (topics) { + try { + await client.repos.replaceAllTopics({ + owner, + repo, + names: topics, + }); + } catch (e) { + ctx.logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`); + } + } + const remoteUrl = newRepo.clone_url; const repoContentsUrl = `${newRepo.html_url}/blob/master`; From 53a883bd147bd6531171fcda8b7b74c0bb873555 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 16 Jun 2021 19:57:16 +0800 Subject: [PATCH 155/206] fix: results are not accurate for search Signed-off-by: Kevin --- .changeset/cyan-drinks-dream.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cyan-drinks-dream.md b/.changeset/cyan-drinks-dream.md index 966de7226a..f07d83bd90 100644 --- a/.changeset/cyan-drinks-dream.md +++ b/.changeset/cyan-drinks-dream.md @@ -2,4 +2,4 @@ '@backstage/plugin-search-backend-node': patch --- -Searching for things like "World" in "Hello World." returns no results. +Improved the quality of free text searches in LunrSearchEngine. From 6f0b0c1a3340fdbe34063c42f660de53b05b55e4 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 16 Jun 2021 14:43:14 +0200 Subject: [PATCH 156/206] Add tests for stemming and trimming Signed-off-by: Oliver Sand --- .../src/engines/LunrSearchEngine.test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index 969e579a53..f27d00097f 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -191,6 +191,72 @@ describe('LunrSearchEngine', () => { }); }); + it('should perform search query with trailing punctuation and return search results on match (trimming)', async () => { + const mockDocuments = [ + { + title: 'testTitle', + text: 'Hello World.', + location: 'test/location', + }, + ]; + + // Mock indexing of 1 document + testLunrSearchEngine.index('test-index', mockDocuments); + + // Perform search query + const mockedSearchResult = await testLunrSearchEngine.query({ + term: 'World', + filters: {}, + pageCursor: '', + }); + + // Should return 1 result as we are mocking the indexing of 1 document with match on the title field + expect(mockedSearchResult).toMatchObject({ + results: [ + { + document: { + title: 'testTitle', + text: 'Hello World.', + location: 'test/location', + }, + }, + ], + }); + }); + + it('should perform search query by similar words and return search results on match (stemming)', async () => { + const mockDocuments = [ + { + title: 'testTitle', + text: 'Searching', + location: 'test/location', + }, + ]; + + // Mock indexing of 1 document + testLunrSearchEngine.index('test-index', mockDocuments); + + // Perform search query + const mockedSearchResult = await testLunrSearchEngine.query({ + term: 'Search', + filters: {}, + pageCursor: '', + }); + + // Should return 1 result as we are mocking the indexing of 1 document with match on the title field + expect(mockedSearchResult).toMatchObject({ + results: [ + { + document: { + title: 'testTitle', + text: 'Searching', + location: 'test/location', + }, + }, + ], + }); + }); + it('should perform search query and return search results on match with filters', async () => { const mockDocuments = [ { From 3b43fc3ab8671327662753d6119aa05a10b178a3 Mon Sep 17 00:00:00 2001 From: Crevil Date: Wed, 16 Jun 2021 16:49:45 +0200 Subject: [PATCH 157/206] Lowercase topics Signed-off-by: Crevil --- .../actions/builtin/publish/github.test.ts | 33 +++++++++++++++++++ .../actions/builtin/publish/github.ts | 3 +- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index c59ba18bea..3dae202643 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -374,6 +374,39 @@ describe('publish:github', () => { }); }); + it('should lowercase topics when provided', async () => { + mockGithubClient.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + mockGithubClient.repos.replaceAllTopics.mockResolvedValue({ + data: { + names: ['backstage'], + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + topics: ['BACKSTAGE'], + }, + }); + + expect(mockGithubClient.repos.replaceAllTopics).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + names: ['backstage'], + }); + }); + it('should call output with the remoteUrl and the repoContentsUrl', async () => { mockGithubClient.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index d7beabe0db..613cbe86d7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -102,7 +102,6 @@ export function createPublishGithubAction(options: { }, topics: { title: 'Topics', - description: 'Uppercase letters no allowed', type: 'array', items: { type: 'string', @@ -230,7 +229,7 @@ export function createPublishGithubAction(options: { await client.repos.replaceAllTopics({ owner, repo, - names: topics, + names: topics.map(t => t.toLowerCase()), }); } catch (e) { ctx.logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`); From 4d63ce7c1b6214121abca9ba0e318470292a0a00 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 16 Jun 2021 17:42:06 +0200 Subject: [PATCH 158/206] core-plugin-api: make useElementFilter filter for undefined component data instead of falsy Signed-off-by: Patrik Oldsberg --- .../src/extensions/useElementFilter.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core-plugin-api/src/extensions/useElementFilter.tsx b/packages/core-plugin-api/src/extensions/useElementFilter.tsx index b33c23e422..fef777cb95 100644 --- a/packages/core-plugin-api/src/extensions/useElementFilter.tsx +++ b/packages/core-plugin-api/src/extensions/useElementFilter.tsx @@ -88,19 +88,21 @@ class ElementCollection { const selection = selectChildren( this.node, this.featureFlagsApi, - node => Boolean(getComponentData(node, query.key)), + node => getComponentData(node, query.key) !== undefined, query.withStrictError, ); return new ElementCollection(selection, this.featureFlagsApi); } findComponentData(query: { key: string }): T[] { - const selection = selectChildren(this.node, this.featureFlagsApi, node => - Boolean(getComponentData(node, query.key)), + const selection = selectChildren( + this.node, + this.featureFlagsApi, + node => getComponentData(node, query.key) !== undefined, ); return selection .map(node => getComponentData(node, query.key)) - .filter((data: T | undefined): data is T => Boolean(data)); + .filter((data: T | undefined): data is T => data !== undefined); } getElements(): Array< From 814b3d0dc278e138898041f3d573acd7fc2a45d5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 16 Jun 2021 17:51:54 +0200 Subject: [PATCH 159/206] core-plugin-api: document useElementFilter Signed-off-by: Patrik Oldsberg --- .../src/extensions/useElementFilter.test.tsx | 4 +- .../src/extensions/useElementFilter.tsx | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/core-plugin-api/src/extensions/useElementFilter.test.tsx b/packages/core-plugin-api/src/extensions/useElementFilter.test.tsx index bb2bcb06a5..73fbb9e5a3 100644 --- a/packages/core-plugin-api/src/extensions/useElementFilter.test.tsx +++ b/packages/core-plugin-api/src/extensions/useElementFilter.test.tsx @@ -59,7 +59,9 @@ describe('useElementFilter', () => { - + + + diff --git a/packages/core-plugin-api/src/extensions/useElementFilter.tsx b/packages/core-plugin-api/src/extensions/useElementFilter.tsx index fef777cb95..e79d2ba508 100644 --- a/packages/core-plugin-api/src/extensions/useElementFilter.tsx +++ b/packages/core-plugin-api/src/extensions/useElementFilter.tsx @@ -84,6 +84,22 @@ class ElementCollection { private readonly featureFlagsApi: FeatureFlagsApi, ) {} + /** + * Narrows the set of selected components by doing a deep traversal and + * only including those that have defined component data for the given `key`. + * + * Whether an element in the tree has component data set for the given key + * is determined by whether `getComponentData` returns undefined. + * + * The traversal does not continue deeper past elements that match the criteria, + * and it also includes the root children in the selection, meaning that if the, + * of all the currently selected elements contain data for the given key, this + * method is a no-op. + * + * If `withStrictError` is set, the resulting selection must be a full match, meaning + * there may be no elements that were excluded in the selection. If the selection + * is not a clean match, an error will be throw with `withStrictError` as the message. + */ selectByComponentData(query: { key: string; withStrictError?: string }) { const selection = selectChildren( this.node, @@ -94,6 +110,10 @@ class ElementCollection { return new ElementCollection(selection, this.featureFlagsApi); } + /** + * Finds all elements using the same criteria as `selectByComponentData`, but + * returns the actual component data of each of those elements instead. + */ findComponentData(query: { key: string }): T[] { const selection = selectChildren( this.node, @@ -105,6 +125,9 @@ class ElementCollection { .filter((data: T | undefined): data is T => data !== undefined); } + /** + * Returns all of the elements currently selected by this collection. + */ getElements(): Array< ReactElement > { @@ -114,6 +137,22 @@ class ElementCollection { } } +/** + * useElementFilter is a utility that helps you narrow down and retrieve data + * from a React element tree, typically operating on the `children` property + * passed in to a component. A common use-case is to construct declarative APIs + * where a React component defines its behavior based on its children, such as + * the relationship between `Routes` and `Route` in `react-router`. + * + * The purpose of this hook is similar to `React.Children.map`, and it expands upon + * it to also handle traversal of fragments and Backstage specific things like the + * `FeatureFlagged` component. + * + * The return value of the hook is computed by the provided filter function, but + * with added memoization based on the input `node`. If further memoization + * dependencies are used in the filter function, they should be added to the + * third `dependencies` argument, just like `useMemo`, `useEffect`, etc. + */ export function useElementFilter( node: ReactNode, filterFn: (arg: ElementCollection) => T, From 64b53d4829e6b398cccb29f6625c95b212eb4386 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 16 Jun 2021 18:31:12 +0200 Subject: [PATCH 160/206] core-plugin-api: export ElementCollection interface + document Signed-off-by: Patrik Oldsberg --- packages/core-plugin-api/api-report.md | 16 +++++- .../core-plugin-api/src/extensions/index.ts | 1 + .../src/extensions/useElementFilter.tsx | 53 +++++++++++++------ 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 6e8cda3d83..90ea1f5e46 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -228,6 +228,20 @@ export type DiscoveryApi = { // @public (undocumented) export const discoveryApiRef: ApiRef; +// @public +export interface ElementCollection { + findComponentData(query: { + key: string; + }): T[]; + getElements(): Array>; + selectByComponentData(query: { + key: string; + withStrictError?: string; + }): ElementCollection; +} + // @public export type ErrorApi = { post(error: Error_2, context?: ErrorContext): void; @@ -515,7 +529,7 @@ export function useApiHolder(): ApiHolder; // @public (undocumented) export const useApp: () => AppContext; -// @public (undocumented) +// @public export function useElementFilter(node: ReactNode, filterFn: (arg: ElementCollection) => T, dependencies?: any[]): T; // @public (undocumented) diff --git a/packages/core-plugin-api/src/extensions/index.ts b/packages/core-plugin-api/src/extensions/index.ts index 71373db1a3..0ee1447b4e 100644 --- a/packages/core-plugin-api/src/extensions/index.ts +++ b/packages/core-plugin-api/src/extensions/index.ts @@ -21,3 +21,4 @@ export { createComponentExtension, } from './extensions'; export { useElementFilter } from './useElementFilter'; +export type { ElementCollection } from './useElementFilter'; diff --git a/packages/core-plugin-api/src/extensions/useElementFilter.tsx b/packages/core-plugin-api/src/extensions/useElementFilter.tsx index e79d2ba508..0549472ab2 100644 --- a/packages/core-plugin-api/src/extensions/useElementFilter.tsx +++ b/packages/core-plugin-api/src/extensions/useElementFilter.tsx @@ -78,12 +78,17 @@ function selectChildren( }); } -class ElementCollection { - constructor( - private readonly node: ReactNode, - private readonly featureFlagsApi: FeatureFlagsApi, - ) {} - +/** + * A querying interface tailored to traversing a set of selected React elements + * and extracting data. + * + * Methods prefixed with `selectBy` are used to narrow the set of selected elements. + * + * Methods prefixed with `find` return concrete data using a deep traversal of the set. + * + * Methods prefixed with `get` return concrete data using a shallow traversal of the set. + */ +export interface ElementCollection { /** * Narrows the set of selected components by doing a deep traversal and * only including those that have defined component data for the given `key`. @@ -100,6 +105,31 @@ class ElementCollection { * there may be no elements that were excluded in the selection. If the selection * is not a clean match, an error will be throw with `withStrictError` as the message. */ + selectByComponentData(query: { + key: string; + withStrictError?: string; + }): ElementCollection; + + /** + * Finds all elements using the same criteria as `selectByComponentData`, but + * returns the actual component data of each of those elements instead. + */ + findComponentData(query: { key: string }): T[]; + + /** + * Returns all of the elements currently selected by this collection. + */ + getElements(): Array< + ReactElement + >; +} + +class Collection implements ElementCollection { + constructor( + private readonly node: ReactNode, + private readonly featureFlagsApi: FeatureFlagsApi, + ) {} + selectByComponentData(query: { key: string; withStrictError?: string }) { const selection = selectChildren( this.node, @@ -107,13 +137,9 @@ class ElementCollection { node => getComponentData(node, query.key) !== undefined, query.withStrictError, ); - return new ElementCollection(selection, this.featureFlagsApi); + return new Collection(selection, this.featureFlagsApi); } - /** - * Finds all elements using the same criteria as `selectByComponentData`, but - * returns the actual component data of each of those elements instead. - */ findComponentData(query: { key: string }): T[] { const selection = selectChildren( this.node, @@ -125,9 +151,6 @@ class ElementCollection { .filter((data: T | undefined): data is T => data !== undefined); } - /** - * Returns all of the elements currently selected by this collection. - */ getElements(): Array< ReactElement > { @@ -159,7 +182,7 @@ export function useElementFilter( dependencies: any[] = [], ) { const featureFlagsApi = useApi(featureFlagsApiRef); - const elements = new ElementCollection(node, featureFlagsApi); + const elements = new Collection(node, featureFlagsApi); // eslint-disable-next-line react-hooks/exhaustive-deps return useMemo(() => filterFn(elements), [node, ...dependencies]); } From 71416fb64ebcd76772d40081890464f4b4754a40 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Wed, 16 Jun 2021 11:29:04 -0600 Subject: [PATCH 161/206] changeset Signed-off-by: Tim Hansen --- .changeset/popular-rice-wonder.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/popular-rice-wonder.md diff --git a/.changeset/popular-rice-wonder.md b/.changeset/popular-rice-wonder.md new file mode 100644 index 0000000000..f815ad949a --- /dev/null +++ b/.changeset/popular-rice-wonder.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Moved installation instructions from the main [backstage.io](https://backstage.io) documentation to the package README file. These instructions are not generally needed, since the plugin comes installed by default with `npx @backstage/create-app`. From 0b22248d758f78a9e5104081ae39edf91b93d33e Mon Sep 17 00:00:00 2001 From: Crevil Date: Wed, 16 Jun 2021 20:44:42 +0200 Subject: [PATCH 162/206] Change change to patch Signed-off-by: Crevil --- .changeset/proud-jars-look.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/proud-jars-look.md b/.changeset/proud-jars-look.md index 0311452b4f..2bae44db66 100644 --- a/.changeset/proud-jars-look.md +++ b/.changeset/proud-jars-look.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-backend': patch --- Add a `topics` input to `publish:github` action that can be used to set topics on the repository upon creation. From c41bb50f94532ee2da5ce87a0df82ff05f7c36ab Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 16 Jun 2021 22:03:51 +0200 Subject: [PATCH 163/206] chore: tidy up docs again Signed-off-by: blam --- docs/auth/identity-resolver.md | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 28bcd7c211..814ff63729 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -5,7 +5,7 @@ description: Identity resolvers of Backstage users after they sign-in --- This guide explains how the identity of a Backstage user is stored inside their -Backstage Identity Token and how you can customize the Sign In resolvers to +Backstage Identity Token and how you can customize the Sign-In resolvers to include identity and group membership information of the user from other external systems. This ultimately helps with determining the ownership of a Backstage entity by a user. The ideas here were originally proposed in the RFC @@ -46,15 +46,14 @@ export default async function createPlugin({ const ent = []; // Let's use the username in the email ID as the user's default - // unique identifier inside Backstage + // unique identifier inside Backstage. const [id] = email.split('@'); - - // Add the unique ID in the list ent.push(`User:default/${id}`) - // Let's call the internal LDAP provider to get a list of groups the user belongs to + // Let's call the internal LDAP provider to get a list of groups + // that the user belongs to, and add those to the list as well const ldapGroups = await getLdapGroups(email); - ldapGroups.forEach(ldapGroup => ent.push(`Group:default/${ldapGroup}`)) + ldapGroups.forEach(group => ent.push(`Group:default/${group}`)) // Issue the token containing the entity claims const token = await ctx.tokenIssuer.issueToken({ @@ -72,10 +71,10 @@ export default async function createPlugin({ As you can see, the generated Backstage Token now contains all the claims about the identity and membership of the user. Once the sign-in process is complete, and we need to find out if a user owns an Entity in the Software Catalog, these -`ent` claims can be used to determine the ownership. A full algorithm as -proposed in the RFC is as follows +`ent` claims can be used to determine the ownership. -The definition of the ownership of an entity E, for a user U, is as follows: +According to the RFC, the definition of the ownership of an entity E, for a user +U, is as follows: - Get all the `ownedBy` relations of E, and call them O - Get all the claims of the user U and call them C @@ -89,15 +88,14 @@ The definition of the ownership of an entity E, for a user U, is as follows: Of course you don't have to customize the sign-in resolver if you don't need to. The Auth backend plugin comes with a set of default sign-in resolvers which you -can use. For example - the Google provider has a default email-based sign in +can use. For example - the Google provider has a default email-based sign-in resolver, which will search the catalog for a single user entity that has a matching `google.com/email` annotation. It can be enabled like this ```tsx -# File: packages/backend/src/plugins/auth.ts -... +// File: packages/backend/src/plugins/auth.ts import { googleEmailSignInResolver } from '@backstage/plugin-auth-backend'; export default async function createPlugin({ @@ -124,10 +122,7 @@ This is also the place where you can do authorization and validation of the user and throw errors if the user should not be allowed access in Backstage. ```tsx -# File: packages/backend/src/plugins/auth.ts -... -import { googleEmailSignInResolver } from '@backstage/plugin-auth-backend'; - +// File: packages/backend/src/plugins/auth.ts export default async function createPlugin({ ... }: PluginEnvironment): Promise { @@ -135,9 +130,6 @@ export default async function createPlugin({ ... providerFactories: { google: createGoogleProvider({ - signIn: { - resolver: googleEmailSignInResolver - }, authHandler: async ({ fullProfile // Type: passport.Profile, idToken // Type: (Optional) string, From 5c4e6aee2536fef8ec00cdeca5b0812ce481449c Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Wed, 16 Jun 2021 19:46:25 -0400 Subject: [PATCH 164/206] feat(explore): customizable explore page Signed-off-by: Andrew Thauer --- .changeset/cuddly-donuts-whisper.md | 54 ++++++++++ plugins/explore/README.md | 46 ++++++++ plugins/explore/package.json | 2 + .../DefaultExplorePage.test.tsx | 63 +++++++++++ .../DefaultExplorePage/DefaultExplorePage.tsx | 45 ++++++++ .../components/DefaultExplorePage/index.ts | 17 +++ .../DomainExplorerContent.test.tsx | 37 ++++--- .../DomainExplorerContent.tsx | 10 +- .../ExploreLayout/ExploreLayout.test.tsx | 81 ++++++++++++++ .../ExploreLayout/ExploreLayout.tsx | 102 ++++++++++++++++++ .../src/components/ExploreLayout/index.ts | 17 +++ .../ExplorePage/ExplorePage.test.tsx | 48 +++++++++ .../components/ExplorePage/ExplorePage.tsx | 19 +--- .../components/ExplorePage/ExploreTabs.tsx | 34 ------ .../GroupsExplorerContent.test.tsx | 31 ++++-- .../GroupsExplorerContent.tsx | 11 +- .../ToolExplorerContent.test.tsx | 12 +++ .../ToolExplorerContent.tsx | 9 +- plugins/explore/src/components/index.ts | 17 +++ plugins/explore/src/extensions.tsx | 38 ++++++- plugins/explore/src/index.ts | 3 +- 21 files changed, 615 insertions(+), 81 deletions(-) create mode 100644 .changeset/cuddly-donuts-whisper.md create mode 100644 plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx create mode 100644 plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx create mode 100644 plugins/explore/src/components/DefaultExplorePage/index.ts create mode 100644 plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx create mode 100644 plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx create mode 100644 plugins/explore/src/components/ExploreLayout/index.ts create mode 100644 plugins/explore/src/components/ExplorePage/ExplorePage.test.tsx delete mode 100644 plugins/explore/src/components/ExplorePage/ExploreTabs.tsx create mode 100644 plugins/explore/src/components/index.ts diff --git a/.changeset/cuddly-donuts-whisper.md b/.changeset/cuddly-donuts-whisper.md new file mode 100644 index 0000000000..fd1b665b99 --- /dev/null +++ b/.changeset/cuddly-donuts-whisper.md @@ -0,0 +1,54 @@ +--- +'@backstage/plugin-explore': patch +--- + +Refactors the explore plugin to be more customizable. This includes the following non-breaking changes: + +- Introduce new `ExploreLayout` page which can be used to create a custom `ExplorePage` +- Refactor `ExplorePage` to use a new `ExploreLayout` component +- Exports existing `DomainExplorerContent`, `GroupsExplorerContent`, & `ToolExplorerContent` components +- Allows `title` props to be customized + +Create a custom explore page in `packages/app/src/components/explore/ExplorePage.tsx`. + +```tsx +import { + DomainExplorerContent, + ExploreLayout, +} from '@backstage/plugin-explore'; +import React from 'react'; +import { InnserSourceExplorerContent } from './InnserSourceExplorerContent'; + +export const ExplorePage = () => { + return ( + + + + + + + + + ); +}; + +export const explorePage = ; +``` + +Now register the new explore page in `packages/app/src/App.tsx`. + +```diff ++ import { explorePage } from './components/explore/ExplorePage'; + +const routes = ( + +- } /> ++ }> ++ {explorePage} ++ + +); +``` diff --git a/plugins/explore/README.md b/plugins/explore/README.md index fea1333e34..8e1cbaed02 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -33,3 +33,49 @@ import LayersIcon from '@material-ui/icons/Layers'; ``` + +## Customization + +Create a custom explore page in `packages/app/src/components/explore/ExplorePage.tsx`. + +```tsx +import { + DomainExplorerContent, + ExploreLayout, +} from '@backstage/plugin-explore'; +import React from 'react'; +import { InnserSourceExplorerContent } from './InnserSourceExplorerContent'; + +export const ExplorePage = () => { + return ( + + + + + + + + + ); +}; + +export const explorePage = ; +``` + +Now register the new explore page in `packages/app/src/App.tsx`. + +```diff ++ import { explorePage } from './components/explore/ExplorePage'; + +const routes = ( + +- } /> ++ }> ++ {explorePage} ++ + +); +``` diff --git a/plugins/explore/package.json b/plugins/explore/package.json index fd6ff19a93..7a7d923c8d 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -38,9 +38,11 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "@types/react": "^16.9", "classnames": "^2.2.6", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx new file mode 100644 index 0000000000..8f69ea3527 --- /dev/null +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -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 { ApiProvider, ApiRegistry } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor, getByText } from '@testing-library/react'; +import React from 'react'; +import { DefaultExplorePage } from './DefaultExplorePage'; + +describe('', () => { + const catalogApi: jest.Mocked = { + addLocation: jest.fn(_a => new Promise(() => {})), + getEntities: jest.fn(), + getOriginLocationByEntity: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + getEntityByName: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders the default explore page', async () => { + catalogApi.getEntities.mockResolvedValue({ items: [] }); + + const { getAllByRole } = await renderInTestApp( + + + , + ); + + await waitFor(() => { + const elements = getAllByRole('tab'); + expect(elements.length).toBe(3); + expect(getByText(elements[0], 'Domains')).toBeInTheDocument(); + expect(getByText(elements[1], 'Groups')).toBeInTheDocument(); + expect(getByText(elements[2], 'Tools')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx new file mode 100644 index 0000000000..abc73dca31 --- /dev/null +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { configApiRef, useApi } from '@backstage/core'; +import { DomainExplorerContent } from '../DomainExplorerContent'; +import { ExploreLayout } from '../ExploreLayout'; +import { GroupsExplorerContent } from '../GroupsExplorerContent'; +import { ToolExplorerContent } from '../ToolExplorerContent'; + +export const DefaultExplorePage = () => { + const configApi = useApi(configApiRef); + const organizationName = + configApi.getOptionalString('organization.name') ?? 'Backstage'; + + return ( + + + + + + + + + + + + ); +}; diff --git a/plugins/explore/src/components/DefaultExplorePage/index.ts b/plugins/explore/src/components/DefaultExplorePage/index.ts new file mode 100644 index 0000000000..b4df272266 --- /dev/null +++ b/plugins/explore/src/components/DefaultExplorePage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DefaultExplorePage } from './DefaultExplorePage'; diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index 1784587a95..1a93463862 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -41,6 +41,12 @@ describe('', () => { ); + const mountedRoutes = { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, + }, + }; + beforeEach(() => { jest.resetAllMocks(); }); @@ -74,11 +80,7 @@ describe('', () => { , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, - }, - }, + mountedRoutes, ); await waitFor(() => { @@ -87,6 +89,19 @@ describe('', () => { }); }); + it('renders a custom title', async () => { + catalogApi.getEntities.mockResolvedValue({ items: [] }); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => expect(getByText('Our Areas')).toBeInTheDocument()); + }); + it('renders empty state', async () => { catalogApi.getEntities.mockResolvedValue({ items: [] }); @@ -94,11 +109,7 @@ describe('', () => { , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, - }, - }, + mountedRoutes, ); await waitFor(() => @@ -114,11 +125,7 @@ describe('', () => { , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, - }, - }, + mountedRoutes, ); await waitFor(() => diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx index a12811e486..8217413842 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx @@ -79,10 +79,16 @@ const Body = () => { ); }; -export const DomainExplorerContent = () => { +type DomainExplorerContentProps = { + title?: string; +}; + +export const DomainExplorerContent = ({ + title, +}: DomainExplorerContentProps) => { return ( - + Discover the domains in your ecosystem. diff --git a/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx b/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx new file mode 100644 index 0000000000..1021ff25d0 --- /dev/null +++ b/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { ExploreLayout } from './ExploreLayout'; + +describe('', () => { + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + <>{children} + ); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders an explore tabbed layout page with defaults', async () => { + const { getByText } = await renderInTestApp( + + + +
Tools Content
+
+
+
, + ); + + await waitFor(() => { + expect(getByText('Explore our ecosystem')).toBeInTheDocument(); + expect( + getByText('Discover solutions available in our ecosystem'), + ).toBeInTheDocument(); + }); + }); + + it('renders a custom page title', async () => { + const { getByText } = await renderInTestApp( + + + +
Tools Content
+
+
+
, + ); + + await waitFor(() => + expect(getByText('Explore our universe')).toBeInTheDocument(), + ); + }); + + it('renders a custom page subtitle', async () => { + const { getByText } = await renderInTestApp( + + + +
Tools Content
+
+
+
, + ); + + await waitFor(() => + expect(getByText('Browse the ACME Corp ecosystem')).toBeInTheDocument(), + ); + }); +}); diff --git a/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx b/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx new file mode 100644 index 0000000000..7e64ed815c --- /dev/null +++ b/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx @@ -0,0 +1,102 @@ +/* + * 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 { attachComponentData, Header, Page, RoutedTabs } from '@backstage/core'; +import { TabProps } from '@material-ui/core'; +import { Children, default as React, Fragment, isValidElement } from 'react'; + +// TODO: This layout could be a shared based component if it was possible to create custom TabbedLayouts +// A generalized version of createSubRoutesFromChildren, etc. would be required + +type SubRoute = { + path: string; + title: string; + children: JSX.Element; + tabProps?: TabProps; +}; + +const Route: (props: SubRoute) => null = () => null; + +// This causes all mount points that are discovered within this route to use the path of the route itself +attachComponentData(Route, 'core.gatherMountPoints', true); + +function createSubRoutesFromChildren( + childrenProps: React.ReactNode, +): SubRoute[] { + // Directly comparing child.type with Route will not work with in + // combination with react-hot-loader in storybook + // https://github.com/gaearon/react-hot-loader/issues/304 + const routeType = ( + +
+ + ).type; + + return Children.toArray(childrenProps).flatMap(child => { + if (!isValidElement(child)) { + return []; + } + + if (child.type === Fragment) { + return createSubRoutesFromChildren(child.props.children); + } + + if (child.type !== routeType) { + throw new Error('Child of ExploreLayout must be an ExploreLayout.Route'); + } + + const { path, title, children, tabProps } = child.props; + return [{ path, title, children, tabProps }]; + }); +} + +type ExploreLayoutProps = { + title?: string; + subtitle?: string; + children?: React.ReactNode; +}; + +/** + * Explore is a compound component, which allows you to define a custom layout + * + * @example + * ```jsx + * + * + *
This is rendered under /example/anything-here route
+ *
+ *
+ * ``` + */ +export const ExploreLayout = ({ + title, + subtitle, + children, +}: ExploreLayoutProps) => { + const routes = createSubRoutesFromChildren(children); + + return ( + +
+ + + ); +}; + +ExploreLayout.Route = Route; diff --git a/plugins/explore/src/components/ExploreLayout/index.ts b/plugins/explore/src/components/ExploreLayout/index.ts new file mode 100644 index 0000000000..6cbae79a71 --- /dev/null +++ b/plugins/explore/src/components/ExploreLayout/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ExploreLayout } from './ExploreLayout'; diff --git a/plugins/explore/src/components/ExplorePage/ExplorePage.test.tsx b/plugins/explore/src/components/ExplorePage/ExplorePage.test.tsx new file mode 100644 index 0000000000..9034ee72c0 --- /dev/null +++ b/plugins/explore/src/components/ExplorePage/ExplorePage.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { useOutlet } from 'react-router'; +import { ExplorePage } from './ExplorePage'; + +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useLocation: jest.fn().mockReturnValue({ + search: '', + }), + useOutlet: jest.fn().mockReturnValue('Route Children'), +})); + +jest.mock('../DefaultExplorePage', () => ({ + ...jest.requireActual('../DefaultExplorePage'), + DefaultExplorePage: jest.fn().mockReturnValue('DefaultExplorePageMock'), +})); + +describe('ExplorePage', () => { + it('renders provided router element', async () => { + const { getByText } = await renderInTestApp(); + + expect(getByText('Route Children')).toBeInTheDocument(); + }); + + it('renders default explorer page when no router children are provided', async () => { + (useOutlet as jest.Mock).mockReturnValueOnce(null); + const { getByText } = await renderInTestApp(); + + expect(getByText('DefaultExplorePageMock')).toBeInTheDocument(); + }); +}); diff --git a/plugins/explore/src/components/ExplorePage/ExplorePage.tsx b/plugins/explore/src/components/ExplorePage/ExplorePage.tsx index 3f9394a1ab..e3601614d2 100644 --- a/plugins/explore/src/components/ExplorePage/ExplorePage.tsx +++ b/plugins/explore/src/components/ExplorePage/ExplorePage.tsx @@ -13,22 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { configApiRef, Header, Page, useApi } from '@backstage/core'; + import React from 'react'; -import { ExploreTabs } from './ExploreTabs'; +import { useOutlet } from 'react-router'; +import { DefaultExplorePage } from '../DefaultExplorePage'; export const ExplorePage = () => { - const configApi = useApi(configApiRef); - const organizationName = - configApi.getOptionalString('organization.name') ?? 'Backstage'; - return ( - -
+ const outlet = useOutlet(); - - - ); + return <>{outlet || }; }; diff --git a/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx b/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx deleted file mode 100644 index 0415dd97ae..0000000000 --- a/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { TabbedLayout } from '@backstage/core'; -import React from 'react'; -import { DomainExplorerContent } from '../DomainExplorerContent'; -import { GroupsExplorerContent } from '../GroupsExplorerContent'; -import { ToolExplorerContent } from '../ToolExplorerContent'; - -export const ExploreTabs = () => ( - - - - - - - - - - - -); diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 38c4678d5a..4e92784818 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -40,6 +40,12 @@ describe('', () => { ); + const mountedRoutes = { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }; + beforeEach(() => { jest.resetAllMocks(); @@ -69,11 +75,7 @@ describe('', () => { , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, - }, + mountedRoutes, ); await waitFor(() => { @@ -81,6 +83,19 @@ describe('', () => { }); }); + it('renders a custom title', async () => { + catalogApi.getEntities.mockResolvedValue({ items: [] }); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => expect(getByText('Our Teams')).toBeInTheDocument()); + }); + it('renders a friendly error if it cannot collect domains', async () => { const catalogError = new Error('Network timeout'); catalogApi.getEntities.mockRejectedValueOnce(catalogError); @@ -89,11 +104,7 @@ describe('', () => { , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, - }, + mountedRoutes, ); await waitFor(() => diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx index c4d46206e4..bf2363f16c 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx @@ -13,14 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Content, ContentHeader, SupportButton } from '@backstage/core'; import React from 'react'; import { GroupsDiagram } from './GroupsDiagram'; -export const GroupsExplorerContent = () => { +type GroupsExplorerContentProps = { + title?: string; +}; + +export const GroupsExplorerContent = ({ + title, +}: GroupsExplorerContentProps) => { return ( - + Explore your groups. diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx index 125a72eb12..5cc444ace5 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx @@ -80,6 +80,18 @@ describe('', () => { }); }); + it('renders a custom title', async () => { + exploreToolsConfigApi.getTools.mockResolvedValue([]); + + const { getByText } = await renderInTestApp( + + + , + ); + + await waitFor(() => expect(getByText('Our Tools')).toBeInTheDocument()); + }); + it('renders empty state', async () => { exploreToolsConfigApi.getTools.mockResolvedValue([]); diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx index e00606eeb2..8dcf1957d0 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Content, ContentHeader, @@ -61,9 +62,13 @@ const Body = () => { ); }; -export const ToolExplorerContent = () => ( +type ToolExplorerContentProps = { + title?: string; +}; + +export const ToolExplorerContent = ({ title }: ToolExplorerContentProps) => ( - + Discover the tools in your ecosystem. diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts new file mode 100644 index 0000000000..6cbae79a71 --- /dev/null +++ b/plugins/explore/src/components/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ExploreLayout } from './ExploreLayout'; diff --git a/plugins/explore/src/extensions.tsx b/plugins/explore/src/extensions.tsx index cdf43d3035..88ea2561fa 100644 --- a/plugins/explore/src/extensions.tsx +++ b/plugins/explore/src/extensions.tsx @@ -14,7 +14,10 @@ * limitations under the License. */ -import { createRoutableExtension } from '@backstage/core'; +import { + createComponentExtension, + createRoutableExtension, +} from '@backstage/core'; import { explorePlugin } from './plugin'; import { exploreRouteRef } from './routes'; @@ -25,3 +28,36 @@ export const ExplorePage = explorePlugin.provide( mountPoint: exploreRouteRef, }), ); + +export const DomainExplorerContent = explorePlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/DomainExplorerContent').then( + m => m.DomainExplorerContent, + ), + }, + }), +); + +export const GroupsExplorerContent = explorePlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/GroupsExplorerContent').then( + m => m.GroupsExplorerContent, + ), + }, + }), +); + +export const ToolExplorerContent = explorePlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/ToolExplorerContent').then( + m => m.ToolExplorerContent, + ), + }, + }), +); diff --git a/plugins/explore/src/index.ts b/plugins/explore/src/index.ts index 70a00f5bbb..bee46a31ec 100644 --- a/plugins/explore/src/index.ts +++ b/plugins/explore/src/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export { ExploreLayout } from './components'; export * from './extensions'; -export { explorePlugin } from './plugin'; +export { explorePlugin, explorePlugin as plugin } from './plugin'; export * from './routes'; From b2fa5d8ba6bd47020fb34249f7fd49d979a3c78b Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 15 Jun 2021 21:04:41 -0600 Subject: [PATCH 165/206] Shorten CHANGELOGs Signed-off-by: Tim Hansen --- .changeset/backstage-changelog.js | 38 +++++++++++++++++++++++++++++++ .changeset/config.json | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .changeset/backstage-changelog.js diff --git a/.changeset/backstage-changelog.js b/.changeset/backstage-changelog.js new file mode 100644 index 0000000000..99c25b80e1 --- /dev/null +++ b/.changeset/backstage-changelog.js @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { + default: defaultChangelogFunctions, +} = require('@changesets/cli/changelog'); + +// Custom CHANGELOG generation for changesets, stolen from here with one minor change: +// https://github.com/atlassian/changesets/blob/main/packages/cli/src/changelog/index.ts +async function getDependencyReleaseLine(changesets, dependenciesUpdated) { + if (dependenciesUpdated.length === 0) return ''; + + const updatedDepenenciesList = dependenciesUpdated.map( + dependency => ` - ${dependency.name}@${dependency.newVersion}`, + ); + + // Return one `Updated dependencies` bullet instead of repeating for each changeset; this + // sacrifices the commit shas for brevity. + return ['- Updated dependencies', ...updatedDepenenciesList].join('\n'); +} + +module.exports = { + getReleaseLine: defaultChangelogFunctions.getReleaseLine, + getDependencyReleaseLine, +}; diff --git a/.changeset/config.json b/.changeset/config.json index 44a8523265..86963b7d09 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@1.3.0/schema.json", - "changelog": "@changesets/cli/changelog", + "changelog": "./backstage-changelog.js", "commit": false, "linked": [["*"]], "access": "public", From 11dfc383496f82f345c7b56a143a020b152873f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Jun 2021 04:07:33 +0000 Subject: [PATCH 166/206] chore(deps): bump humanize-duration from 3.26.0 to 3.27.0 Bumps [humanize-duration](https://github.com/EvanHahn/HumanizeDuration.js) from 3.26.0 to 3.27.0. - [Release notes](https://github.com/EvanHahn/HumanizeDuration.js/releases) - [Changelog](https://github.com/EvanHahn/HumanizeDuration.js/blob/main/HISTORY.md) - [Commits](https://github.com/EvanHahn/HumanizeDuration.js/compare/v3.26.0...v3.27.0) --- updated-dependencies: - dependency-name: humanize-duration dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7fd562c64d..bf435c483c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14921,15 +14921,10 @@ human-signals@^2.1.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -humanize-duration@^3.25.1: - version "3.25.1" - resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.25.1.tgz#50e12bf4b3f515ec91106107ee981e8cfe955d6f" - integrity sha512-P+dRo48gpLgc2R9tMRgiDRNULPKCmqFYgguwqOO2C0fjO35TgdURDQDANSR1Nt92iHlbHGMxOTnsB8H8xnMa2Q== - -humanize-duration@^3.26.0: - version "3.26.0" - resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.26.0.tgz#4d77f6b3d2fe0ca1ff14623ccc2b2f8b48ab1aaf" - integrity sha512-SddekX3p5ApvPY6bbAYppGKe874jP6iFZXYtrQToDV4R0j2UpTYPqwTFM2QpXpuw9DhS/eXTUnKYTF9TbXAJ6A== +humanize-duration@^3.25.1, humanize-duration@^3.26.0: + version "3.27.0" + resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.0.tgz#3f781b7cf8022ad587f76b9839b60bc2b29636b2" + integrity sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ== humanize-ms@^1.2.1: version "1.2.1" From 27ed95a878cb1906e8b7d7627c2674aaa9535c6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Jun 2021 04:10:07 +0000 Subject: [PATCH 167/206] chore(deps): bump testcontainers from 7.11.0 to 7.11.1 Bumps [testcontainers](https://github.com/testcontainers/testcontainers-node) from 7.11.0 to 7.11.1. - [Release notes](https://github.com/testcontainers/testcontainers-node/releases) - [Commits](https://github.com/testcontainers/testcontainers-node/compare/v7.11.0...v7.11.1) --- updated-dependencies: - dependency-name: testcontainers dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 50 +++++++++----------------------------------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7fd562c64d..ab6a157c6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7656,20 +7656,7 @@ archiver-utils@^2.1.0: normalize-path "^3.0.0" readable-stream "^2.0.0" -archiver@^5.0.2: - version "5.2.0" - resolved "https://registry.npmjs.org/archiver/-/archiver-5.2.0.tgz#25aa1b3d9febf7aec5b0f296e77e69960c26db94" - integrity sha512-QEAKlgQuAtUxKeZB9w5/ggKXh21bZS+dzzuQ0RPBC20qtDCbTyzqmisoeJP46MP39fg4B4IcyvR+yeyEBdblsQ== - dependencies: - archiver-utils "^2.1.0" - async "^3.2.0" - buffer-crc32 "^0.2.1" - readable-stream "^3.6.0" - readdir-glob "^1.0.0" - tar-stream "^2.1.4" - zip-stream "^4.0.4" - -archiver@^5.3.0: +archiver@^5.0.2, archiver@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== @@ -9866,16 +9853,6 @@ component-emitter@^1.2.0, component-emitter@^1.2.1, component-emitter@^1.3.0: resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== -compress-commons@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.2.tgz#d6896be386e52f37610cef9e6fa5defc58c31bd7" - integrity sha512-qhd32a9xgzmpfoga1VQEiLEwdKZ6Plnpx5UCgIsf89FSolyJ7WnifY4Gtjgv5WR6hWAyRaHxC5MiEhU/38U70A== - dependencies: - buffer-crc32 "^0.2.13" - crc32-stream "^4.0.1" - normalize-path "^3.0.0" - readable-stream "^3.6.0" - compress-commons@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.0.tgz#25ec7a4528852ccd1d441a7d4353cd0ece11371b" @@ -11464,10 +11441,10 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" -docker-compose@^0.23.8: - version "0.23.10" - resolved "https://registry.npmjs.org/docker-compose/-/docker-compose-0.23.10.tgz#369fd2c6429754fb4134d3d29174a8c9569690e8" - integrity sha512-IzR6LzHrQyUvVwPNZY6F0oszAQLqHKOMNTN43Yu5aE6IBbhN9D/MpHbVUqHXTwzqIWiJM+ImYFjY5RdWWDGgfQ== +docker-compose@^0.23.10: + version "0.23.12" + resolved "https://registry.npmjs.org/docker-compose/-/docker-compose-0.23.12.tgz#fa883b98be08f6926143d06bf9e522ef7ed3210c" + integrity sha512-KFbSMqQBuHjTGZGmYDOCO0L4SaML3BsWTId5oSUyaBa22vALuFHNv+UdDWs3HcMylHWKsxCbLB7hnM/nCosWZw== dependencies: yaml "^1.10.2" @@ -25082,16 +25059,16 @@ test-exclude@^6.0.0: minimatch "^3.0.4" testcontainers@^7.10.0: - version "7.11.0" - resolved "https://registry.npmjs.org/testcontainers/-/testcontainers-7.11.0.tgz#47291a7e693b6d8f7a4f03cd0fb2a6a741c53458" - integrity sha512-wnTS/foBu3lFjLHRCQLv+nSBnzgp20tWRJVCRXFzU6ivDnFwNbyLMZiHhHZr6hpXtCNAToOlBenlueDA5z1L7g== + version "7.11.1" + resolved "https://registry.npmjs.org/testcontainers/-/testcontainers-7.11.1.tgz#b3810badf79433ba02f210683eec1a1c488c107d" + integrity sha512-lfZeys5bLkADjOaoXfQy0V0+G8sGKr8ESANz7MhSVBwC+OTTxkP3+FVwP48bW4mwRcQ4Hojwbfw10OYT80QZmQ== dependencies: "@types/archiver" "^5.1.0" "@types/dockerode" "^3.2.1" archiver "^5.3.0" byline "^5.0.0" debug "^4.3.1" - docker-compose "^0.23.8" + docker-compose "^0.23.10" dockerode "^3.2.1" get-port "^5.1.1" glob "^7.1.7" @@ -27198,15 +27175,6 @@ zenscroll@^4.0.2: resolved "https://registry.npmjs.org/zenscroll/-/zenscroll-4.0.2.tgz#e8d5774d1c0738a47bcfa8729f3712e2deddeb25" integrity sha1-6NV3TRwHOKR7z6hynzcS4t7d6yU= -zip-stream@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/zip-stream/-/zip-stream-4.0.4.tgz#3a8f100b73afaa7d1ae9338d910b321dec77ff3a" - integrity sha512-a65wQ3h5gcQ/nQGWV1mSZCEzCML6EK/vyVPcrPNynySP1j3VBbQKh3nhC8CbORb+jfl2vXvh56Ul5odP1bAHqw== - dependencies: - archiver-utils "^2.1.0" - compress-commons "^4.0.2" - readable-stream "^3.6.0" - zip-stream@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" From 81ec239bb11196561b45ba4dd7d2c7e06b56c349 Mon Sep 17 00:00:00 2001 From: Bryan Lam Date: Wed, 16 Jun 2021 15:24:41 -0700 Subject: [PATCH 168/206] Fix backend-plugin document typo Signed-off-by: Bryan Lam --- docs/plugins/backend-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 7a57cefa9c..1d728b76aa 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -99,7 +99,7 @@ import carmen from './plugins/carmen'; async function main() { // ... const carmenEnv = useHotMemoize(module, () => createEnv('carmen')); - apiRouter.use('/carmen', await carmen(badgesEnv)); + apiRouter.use('/carmen', await carmen(carmenEnv)); ``` After you start the backend (e.g. using `yarn start-backend` from the repo From ca1b96004311e6a2da14eb6085b98968dec55269 Mon Sep 17 00:00:00 2001 From: Dorn- Date: Wed, 16 Jun 2021 21:49:01 +0200 Subject: [PATCH 169/206] fix: typo in openStackSwift credentials username A typo was made inside the credentials used by openStackSwift publisher. As you can see here: https://github.com/backstage/backstage/blob/master/packages/techdocs-common/src/stages/publish/openStackSwift.ts#L67 or https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/config.d.ts#L119 Signed-off-by: Flavien Chantelot Signed-off-by: Flavien Chantelot --- docs/features/techdocs/using-cloud-storage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 62ee4867c8..119629915e 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -370,7 +370,7 @@ techdocs: openStackSwift: containerName: 'name-of-techdocs-storage-bucket' credentials: - userName: ${OPENSTACK_SWIFT_STORAGE_USERNAME} + username: ${OPENSTACK_SWIFT_STORAGE_USERNAME} password: ${OPENSTACK_SWIFT_STORAGE_PASSWORD} authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL} keystoneAuthVersion: ${OPENSTACK_SWIFT_STORAGE_AUTH_VERSION} From 23ad991a173d1eea23213658a1450d2bd38c7cf5 Mon Sep 17 00:00:00 2001 From: Crevil Date: Thu, 17 Jun 2021 09:20:45 +0200 Subject: [PATCH 170/206] Add example of visibility of config values Emphasize how to set config visibility in the docs with an example. Signed-off-by: Crevil --- docs/conf/defining.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/conf/defining.md b/docs/conf/defining.md index 34b9b11977..f13beb9f77 100644 --- a/docs/conf/defining.md +++ b/docs/conf/defining.md @@ -93,6 +93,21 @@ declare the visibility of a leaf node of `type: "string"`. | `backend` | (Default) Only in backend | | `secret` | Only in backend and may be excluded from logs for security reasons | +You can set visibility with an `@visibility` comment in the `Config` Typescript +interface. + +```ts +export interface Config { + app: { + /** + * Frontend root URL + * @visibility frontend + */ + baseUrl: string; + }; +} +``` + ## Validation Schemas can be validated using the `backstage-cli config:check` command. If you From 0606f9b416fcdeb674b295bdd89d8cd87be02d0d Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 17 Jun 2021 09:30:36 +0200 Subject: [PATCH 171/206] Clean up tests. Signed-off-by: Eric Peterson --- .../reader/transformers/addBaseUrl.test.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index 5a3205f886..6b6eae4b0d 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { waitFor } from '@testing-library/react'; import { createTestShadowDom } from '../../test-utils'; import { addBaseUrl } from '../transformers'; import { TechDocsStorageApi } from '../../api'; @@ -120,12 +121,9 @@ describe('addBaseUrl', () => { postTransformers: [], }); - await new Promise(done => { - process.nextTick(() => { - const actualSrc = root.getElementById('x')?.getAttribute('src'); - expect(expectedSrc).toEqual(actualSrc); - done(); - }); + await waitFor(() => { + const actualSrc = root.getElementById('x')?.getAttribute('src'); + expect(expectedSrc).toEqual(actualSrc); }); }); @@ -153,12 +151,9 @@ describe('addBaseUrl', () => { }, ); - await new Promise(done => { - process.nextTick(() => { - const actualSrc = root.getElementById('x')?.getAttribute('src'); - expect(expectedSrc).toEqual(actualSrc); - done(); - }); + await waitFor(() => { + const actualSrc = root.getElementById('x')?.getAttribute('src'); + expect(expectedSrc).toEqual(actualSrc); }); }); From 36e5a82e9b159bb0d025c0f3d18a3de947d72db0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 17 Jun 2021 08:08:44 +0000 Subject: [PATCH 172/206] Version Packages --- .changeset/chilly-ants-taste.md | 5 - .changeset/chilly-owls-punch.md | 5 - .changeset/clean-frogs-brake.md | 10 -- .changeset/cyan-drinks-dream.md | 5 - .changeset/dull-poets-learn.md | 52 --------- .changeset/fair-points-grin.md | 5 - .changeset/fast-trees-arrive.md | 24 ---- .changeset/fifty-tigers-learn.md | 5 - .changeset/five-donkeys-brake.md | 51 --------- .changeset/flat-chefs-push.md | 5 - .changeset/flat-dolls-search.md | 5 - .changeset/fresh-vans-nail.md | 5 - .changeset/funny-toys-talk.md | 5 - .changeset/fuzzy-jobs-relate.md | 5 - .changeset/gorgeous-pumas-tickle.md | 5 - .changeset/healthy-windows-dance.md | 5 - .changeset/honest-parents-join.md | 9 -- .changeset/honest-pianos-smell.md | 5 - .changeset/honest-rabbits-divide.md | 5 - .changeset/kind-tools-kneel.md | 7 -- .changeset/lazy-cougars-rule.md | 5 - .changeset/mean-moose-sneeze.md | 5 - .changeset/nasty-wasps-look.md | 5 - .changeset/nice-dryers-dream.md | 5 - .changeset/nice-spoons-try.md | 5 - .changeset/ninety-horses-rescue.md | 5 - .changeset/pink-llamas-sniff.md | 12 -- .changeset/proud-jars-look.md | 5 - .changeset/purple-papayas-exist.md | 5 - .changeset/seven-wolves-clean.md | 5 - .changeset/shaggy-vans-travel.md | 5 - .changeset/sharp-candles-type.md | 5 - .changeset/tall-bears-taste.md | 5 - .changeset/techdocs-a-primeira-vez.md | 6 - .changeset/techdocs-metal-clouds-work.md | 5 - .changeset/thick-donkeys-carry.md | 5 - .changeset/thick-donkeys-fold.md | 5 - .changeset/thirty-turkeys-sing.md | 5 - .changeset/tough-ravens-change.md | 5 - .changeset/violet-ads-change.md | 5 - .changeset/violet-birds-lay.md | 5 - .changeset/wild-ghosts-deny.md | 5 - .changeset/yellow-schools-matter.md | 6 - packages/app/CHANGELOG.md | 18 +++ packages/app/package.json | 26 ++--- packages/backend-common/CHANGELOG.md | 54 +++++++++ packages/backend-common/package.json | 6 +- packages/backend-test-utils/CHANGELOG.md | 54 +++++++++ packages/backend-test-utils/package.json | 8 +- packages/catalog-model/CHANGELOG.md | 6 + packages/catalog-model/package.json | 4 +- packages/cli/CHANGELOG.md | 28 +++++ packages/cli/package.json | 8 +- packages/codemods/CHANGELOG.md | 8 ++ packages/codemods/package.json | 2 +- packages/config-loader/CHANGELOG.md | 6 + packages/config-loader/package.json | 2 +- packages/core-components/CHANGELOG.md | 7 ++ packages/core-components/package.json | 4 +- packages/core/CHANGELOG.md | 6 + packages/core/package.json | 4 +- packages/create-app/CHANGELOG.md | 125 +++++++++++++++++++++ packages/create-app/package.json | 2 +- plugins/api-docs/CHANGELOG.md | 15 +++ plugins/api-docs/package.json | 12 +- plugins/app-backend/CHANGELOG.md | 9 ++ plugins/app-backend/package.json | 8 +- plugins/auth-backend/CHANGELOG.md | 9 ++ plugins/auth-backend/package.json | 8 +- plugins/badges-backend/CHANGELOG.md | 9 ++ plugins/badges-backend/package.json | 8 +- plugins/badges/package.json | 2 +- plugins/bitrise/package.json | 2 +- plugins/catalog-backend/CHANGELOG.md | 65 +++++++++++ plugins/catalog-backend/package.json | 12 +- plugins/catalog-import/CHANGELOG.md | 10 ++ plugins/catalog-import/package.json | 10 +- plugins/catalog-react/CHANGELOG.md | 9 ++ plugins/catalog-react/package.json | 10 +- plugins/catalog/CHANGELOG.md | 13 +++ plugins/catalog/package.json | 10 +- plugins/circleci/CHANGELOG.md | 10 ++ plugins/circleci/package.json | 10 +- plugins/cloudbuild/package.json | 2 +- plugins/code-coverage-backend/CHANGELOG.md | 9 ++ plugins/code-coverage-backend/package.json | 8 +- plugins/code-coverage/package.json | 2 +- plugins/config-schema/package.json | 2 +- plugins/cost-insights/package.json | 2 +- plugins/explore/package.json | 2 +- plugins/fossa/package.json | 2 +- plugins/gcp-projects/package.json | 2 +- plugins/git-release-manager/package.json | 2 +- plugins/github-actions/package.json | 2 +- plugins/github-deployments/package.json | 2 +- plugins/gitops-profiles/package.json | 2 +- plugins/graphiql/package.json | 2 +- plugins/ilert/package.json | 2 +- plugins/jenkins/CHANGELOG.md | 10 ++ plugins/jenkins/package.json | 10 +- plugins/kafka/package.json | 2 +- plugins/kubernetes/package.json | 2 +- plugins/lighthouse/package.json | 2 +- plugins/newrelic/package.json | 2 +- plugins/org/package.json | 2 +- plugins/pagerduty/package.json | 2 +- plugins/proxy-backend/CHANGELOG.md | 9 ++ plugins/proxy-backend/package.json | 6 +- plugins/register-component/package.json | 2 +- plugins/rollbar-backend/CHANGELOG.md | 8 ++ plugins/rollbar-backend/package.json | 6 +- plugins/rollbar/package.json | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 19 ++++ plugins/scaffolder-backend/package.json | 8 +- plugins/scaffolder/package.json | 2 +- plugins/search-backend-node/CHANGELOG.md | 6 + plugins/search-backend-node/package.json | 6 +- plugins/search-backend/CHANGELOG.md | 9 ++ plugins/search-backend/package.json | 8 +- plugins/search/package.json | 2 +- plugins/sentry/CHANGELOG.md | 10 ++ plugins/sentry/package.json | 10 +- plugins/shortcuts/package.json | 2 +- plugins/sonarqube/package.json | 2 +- plugins/splunk-on-call/CHANGELOG.md | 10 ++ plugins/splunk-on-call/package.json | 10 +- plugins/tech-radar/package.json | 2 +- plugins/techdocs-backend/CHANGELOG.md | 10 ++ plugins/techdocs-backend/package.json | 8 +- plugins/techdocs/CHANGELOG.md | 14 +++ plugins/techdocs/package.json | 10 +- plugins/todo-backend/CHANGELOG.md | 9 ++ plugins/todo-backend/package.json | 8 +- plugins/todo/package.json | 2 +- plugins/user-settings/CHANGELOG.md | 8 ++ plugins/user-settings/package.json | 6 +- plugins/welcome/package.json | 2 +- 137 files changed, 751 insertions(+), 506 deletions(-) delete mode 100644 .changeset/chilly-ants-taste.md delete mode 100644 .changeset/chilly-owls-punch.md delete mode 100644 .changeset/clean-frogs-brake.md delete mode 100644 .changeset/cyan-drinks-dream.md delete mode 100644 .changeset/dull-poets-learn.md delete mode 100644 .changeset/fair-points-grin.md delete mode 100644 .changeset/fast-trees-arrive.md delete mode 100644 .changeset/fifty-tigers-learn.md delete mode 100644 .changeset/five-donkeys-brake.md delete mode 100644 .changeset/flat-chefs-push.md delete mode 100644 .changeset/flat-dolls-search.md delete mode 100644 .changeset/fresh-vans-nail.md delete mode 100644 .changeset/funny-toys-talk.md delete mode 100644 .changeset/fuzzy-jobs-relate.md delete mode 100644 .changeset/gorgeous-pumas-tickle.md delete mode 100644 .changeset/healthy-windows-dance.md delete mode 100644 .changeset/honest-parents-join.md delete mode 100644 .changeset/honest-pianos-smell.md delete mode 100644 .changeset/honest-rabbits-divide.md delete mode 100644 .changeset/kind-tools-kneel.md delete mode 100644 .changeset/lazy-cougars-rule.md delete mode 100644 .changeset/mean-moose-sneeze.md delete mode 100644 .changeset/nasty-wasps-look.md delete mode 100644 .changeset/nice-dryers-dream.md delete mode 100644 .changeset/nice-spoons-try.md delete mode 100644 .changeset/ninety-horses-rescue.md delete mode 100644 .changeset/pink-llamas-sniff.md delete mode 100644 .changeset/proud-jars-look.md delete mode 100644 .changeset/purple-papayas-exist.md delete mode 100644 .changeset/seven-wolves-clean.md delete mode 100644 .changeset/shaggy-vans-travel.md delete mode 100644 .changeset/sharp-candles-type.md delete mode 100644 .changeset/tall-bears-taste.md delete mode 100644 .changeset/techdocs-a-primeira-vez.md delete mode 100644 .changeset/techdocs-metal-clouds-work.md delete mode 100644 .changeset/thick-donkeys-carry.md delete mode 100644 .changeset/thick-donkeys-fold.md delete mode 100644 .changeset/thirty-turkeys-sing.md delete mode 100644 .changeset/tough-ravens-change.md delete mode 100644 .changeset/violet-ads-change.md delete mode 100644 .changeset/violet-birds-lay.md delete mode 100644 .changeset/wild-ghosts-deny.md delete mode 100644 .changeset/yellow-schools-matter.md diff --git a/.changeset/chilly-ants-taste.md b/.changeset/chilly-ants-taste.md deleted file mode 100644 index e9422e0317..0000000000 --- a/.changeset/chilly-ants-taste.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Add `EntityLifecyclePicker` and `EntityOwnerPicker` UI components to allow filtering by `spec.lifecycle` and `spec.owner` on catalog-related pages. diff --git a/.changeset/chilly-owls-punch.md b/.changeset/chilly-owls-punch.md deleted file mode 100644 index 35fb581a81..0000000000 --- a/.changeset/chilly-owls-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Exports `CatalogLayout` and `CreateComponentButton` for catalog customization. diff --git a/.changeset/clean-frogs-brake.md b/.changeset/clean-frogs-brake.md deleted file mode 100644 index ccbe551853..0000000000 --- a/.changeset/clean-frogs-brake.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Adding .DS_Store pattern to .gitignore in Scaffolded Backstage App. To migrate an existing app that pattern should be added manually. - -```diff -+# macOS -+.DS_Store -``` diff --git a/.changeset/cyan-drinks-dream.md b/.changeset/cyan-drinks-dream.md deleted file mode 100644 index f07d83bd90..0000000000 --- a/.changeset/cyan-drinks-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend-node': patch ---- - -Improved the quality of free text searches in LunrSearchEngine. diff --git a/.changeset/dull-poets-learn.md b/.changeset/dull-poets-learn.md deleted file mode 100644 index b504c0c6a3..0000000000 --- a/.changeset/dull-poets-learn.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch -'@backstage/create-app': patch ---- - -This release enables the new catalog processing engine which is a major milestone for the catalog! - -This update makes processing more scalable across multiple instances, adds support for deletions and ui flagging of entities that are no longer referenced by a location. - -**Changes Required** to `catalog.ts` - -```diff --import { useHotCleanup } from '@backstage/backend-common'; - import { - CatalogBuilder, -- createRouter, -- runPeriodically -+ createRouter - } from '@backstage/plugin-catalog-backend'; - import { Router } from 'express'; - import { PluginEnvironment } from '../types'; - - export default async function createPlugin(env: PluginEnvironment): Promise { -- const builder = new CatalogBuilder(env); -+ const builder = await CatalogBuilder.create(env); - const { - entitiesCatalog, - locationsCatalog, -- higherOrderOperation, -+ locationService, -+ processingEngine, - locationAnalyzer, - } = await builder.build(); - -- useHotCleanup( -- module, -- runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000), -- ); -+ await processingEngine.start(); - - return await createRouter({ - entitiesCatalog, - locationsCatalog, -- higherOrderOperation, -+ locationService, - locationAnalyzer, - logger: env.logger, - config: env.config, -``` - -As this is a major internal change we have taken some precaution by still allowing the old catalog to be enabled by keeping your `catalog.ts` in it's current state. -If you encounter any issues and have to revert to the previous catalog engine make sure to raise an issue immediately as the old catalog engine is deprecated and will be removed in a future release. diff --git a/.changeset/fair-points-grin.md b/.changeset/fair-points-grin.md deleted file mode 100644 index 5c5b21b610..0000000000 --- a/.changeset/fair-points-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Provide a more clear error message when database connection fails. diff --git a/.changeset/fast-trees-arrive.md b/.changeset/fast-trees-arrive.md deleted file mode 100644 index f74b6c1c9a..0000000000 --- a/.changeset/fast-trees-arrive.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Make `yarn dev` in newly created backend plugins respect the `PLUGIN_PORT` environment variable. - -You can achieve the same in your created backend plugins by making sure to properly call the port and CORS methods on your service builder. Typically in a file named `src/service/standaloneServer.ts` inside your backend plugin package, replace the following: - -```ts -const service = createServiceBuilder(module) - .enableCors({ origin: 'http://localhost:3000' }) - .addRouter('/my-plugin', router); -``` - -With something like the following: - -```ts -let service = createServiceBuilder(module) - .setPort(options.port) - .addRouter('/my-plugin', router); -if (options.enableCors) { - service = service.enableCors({ origin: 'http://localhost:3000' }); -} -``` diff --git a/.changeset/fifty-tigers-learn.md b/.changeset/fifty-tigers-learn.md deleted file mode 100644 index 465c8c54a5..0000000000 --- a/.changeset/fifty-tigers-learn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-api-docs': minor ---- - -Rework `ApiExplorerPage` to utilize `EntityListProvider` to provide a consistent UI with the `CatalogIndexPage` which now exposes support for starring entities, pagination, and customizing columns. diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md deleted file mode 100644 index 274e704888..0000000000 --- a/.changeset/five-donkeys-brake.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/create-app': patch -'@backstage/backend-test-utils': patch ---- - -Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database -connection manager, `DatabaseManager`, which allows developers to configure database -connections on a per plugin basis. - -The `backend.database` config path allows you to set `prefix` to use an -alternate prefix for automatically generated database names, the default is -`backstage_plugin_`. Use `backend.database.plugin.` to set plugin -specific database connection configuration, e.g. - -```yaml -backend: - database: - client: 'pg', - prefix: 'custom_prefix_' - connection: - host: 'localhost' - user: 'foo' - password: 'bar' - plugin: - catalog: - connection: - database: 'database_name_overriden' - scaffolder: - client: 'sqlite3' - connection: ':memory:' -``` - -Migrate existing backstage installations by swapping out the database manager in the -`packages/backend/src/index.ts` file as shown below: - -```diff -import { -- SingleConnectionDatabaseManager, -+ DatabaseManager, -} from '@backstage/backend-common'; - -// ... - -function makeCreateEnv(config: Config) { - // ... -- const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); -+ const databaseManager = DatabaseManager.fromConfig(config); - // ... -} -``` diff --git a/.changeset/flat-chefs-push.md b/.changeset/flat-chefs-push.md deleted file mode 100644 index 98ee448dba..0000000000 --- a/.changeset/flat-chefs-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Truncate long entity names on the system diagram diff --git a/.changeset/flat-dolls-search.md b/.changeset/flat-dolls-search.md deleted file mode 100644 index 4c09b64168..0000000000 --- a/.changeset/flat-dolls-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-todo-backend': patch ---- - -Bump `leasot` dependency from 11.5.0 to 12.0.0, removing support for Node.js version 10. diff --git a/.changeset/fresh-vans-nail.md b/.changeset/fresh-vans-nail.md deleted file mode 100644 index 64af4ee60b..0000000000 --- a/.changeset/fresh-vans-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-splunk-on-call': patch ---- - -Added config schema to expose `splunkOnCall.eventsRestEndpoint` config option to the frontend diff --git a/.changeset/funny-toys-talk.md b/.changeset/funny-toys-talk.md deleted file mode 100644 index 6e6a779ab8..0000000000 --- a/.changeset/funny-toys-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/codemods': patch ---- - -Fix execution of `jscodeshift` on windows. diff --git a/.changeset/fuzzy-jobs-relate.md b/.changeset/fuzzy-jobs-relate.md deleted file mode 100644 index 6828d6c3c0..0000000000 --- a/.changeset/fuzzy-jobs-relate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Switches the default catalog processing engine to use a batched streaming task execution strategy for higher parallelism. diff --git a/.changeset/gorgeous-pumas-tickle.md b/.changeset/gorgeous-pumas-tickle.md deleted file mode 100644 index 2f653fae84..0000000000 --- a/.changeset/gorgeous-pumas-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Rely on `SELECT ... FOR UPDATE SKIP LOCKED` where available in order to speed up processing item acquisition and reduce work duplication. diff --git a/.changeset/healthy-windows-dance.md b/.changeset/healthy-windows-dance.md deleted file mode 100644 index 1015fb63c3..0000000000 --- a/.changeset/healthy-windows-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Use the correct parameter to create a public repository in Bitbucket Server for the v2 templates diff --git a/.changeset/honest-parents-join.md b/.changeset/honest-parents-join.md deleted file mode 100644 index 19e77d617c..0000000000 --- a/.changeset/honest-parents-join.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Describe `publish:github` scaffolder action fields - -This change adds a description to the fields with examples of what to input. The -`collaborators` description is also expanded a bit to make it more clear that -these are additional compared to access and owner. diff --git a/.changeset/honest-pianos-smell.md b/.changeset/honest-pianos-smell.md deleted file mode 100644 index 56b87ce72d..0000000000 --- a/.changeset/honest-pianos-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins': patch ---- - -Support showing build details for branches with slashes in their names diff --git a/.changeset/honest-rabbits-divide.md b/.changeset/honest-rabbits-divide.md deleted file mode 100644 index bf4b457383..0000000000 --- a/.changeset/honest-rabbits-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fix the link to the documentation page when no owned documents are displayed diff --git a/.changeset/kind-tools-kneel.md b/.changeset/kind-tools-kneel.md deleted file mode 100644 index 6296ac7011..0000000000 --- a/.changeset/kind-tools-kneel.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Make refresh interval configurable for the `NextCatalogBuilder` using `.setRefreshIntervalSeconds()`. - -Change `DefaultProcessingDatabase` constructor to accept an options object instead of individual arguments. diff --git a/.changeset/lazy-cougars-rule.md b/.changeset/lazy-cougars-rule.md deleted file mode 100644 index cc6ec91bd5..0000000000 --- a/.changeset/lazy-cougars-rule.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Adds support to enable LFS for hosted Bitbucket diff --git a/.changeset/mean-moose-sneeze.md b/.changeset/mean-moose-sneeze.md deleted file mode 100644 index 2b0d66ffb2..0000000000 --- a/.changeset/mean-moose-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-circleci': patch ---- - -Remove moment as part 1 of migration to `lexon` diff --git a/.changeset/nasty-wasps-look.md b/.changeset/nasty-wasps-look.md deleted file mode 100644 index 6de02cbb04..0000000000 --- a/.changeset/nasty-wasps-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': patch ---- - -TechDocs: Support configurable working directory as temp dir diff --git a/.changeset/nice-dryers-dream.md b/.changeset/nice-dryers-dream.md deleted file mode 100644 index a447bb564a..0000000000 --- a/.changeset/nice-dryers-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Implement `NextCatalogBuilder.addEntityProvider` diff --git a/.changeset/nice-spoons-try.md b/.changeset/nice-spoons-try.md deleted file mode 100644 index c0992fa05e..0000000000 --- a/.changeset/nice-spoons-try.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-import': patch ---- - -Fix a react warning in ``. diff --git a/.changeset/ninety-horses-rescue.md b/.changeset/ninety-horses-rescue.md deleted file mode 100644 index b3e0decdde..0000000000 --- a/.changeset/ninety-horses-rescue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sentry': patch ---- - -Migrated the package from `timeago.js` to `luxon`. See #4278 diff --git a/.changeset/pink-llamas-sniff.md b/.changeset/pink-llamas-sniff.md deleted file mode 100644 index 98addd691c..0000000000 --- a/.changeset/pink-llamas-sniff.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/plugin-app-backend': patch -'@backstage/plugin-badges-backend': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-code-coverage-backend': patch -'@backstage/plugin-proxy-backend': patch -'@backstage/plugin-rollbar-backend': patch -'@backstage/plugin-search-backend': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Make `yarn dev` respect the `PLUGIN_PORT` environment variable. diff --git a/.changeset/proud-jars-look.md b/.changeset/proud-jars-look.md deleted file mode 100644 index 2bae44db66..0000000000 --- a/.changeset/proud-jars-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Add a `topics` input to `publish:github` action that can be used to set topics on the repository upon creation. diff --git a/.changeset/purple-papayas-exist.md b/.changeset/purple-papayas-exist.md deleted file mode 100644 index 4c77558476..0000000000 --- a/.changeset/purple-papayas-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Fix a bug that prevented changing themes on the user settings page when the theme `id` didn't match exactly the theme `variant`. diff --git a/.changeset/seven-wolves-clean.md b/.changeset/seven-wolves-clean.md deleted file mode 100644 index 3b807b1af6..0000000000 --- a/.changeset/seven-wolves-clean.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Do not add trailing slash for .html pages during doc links rewriting diff --git a/.changeset/shaggy-vans-travel.md b/.changeset/shaggy-vans-travel.md deleted file mode 100644 index 898731f584..0000000000 --- a/.changeset/shaggy-vans-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch ---- - -Add pagination to ApiExplorerTable diff --git a/.changeset/sharp-candles-type.md b/.changeset/sharp-candles-type.md deleted file mode 100644 index 259e2151c0..0000000000 --- a/.changeset/sharp-candles-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': patch ---- - -Removed workaround for breaking change in typescript 4.3 and bump `typescript-json-schema` instead. This should again allow the usage of `@items.visibility ` to set the visibility of array items. diff --git a/.changeset/tall-bears-taste.md b/.changeset/tall-bears-taste.md deleted file mode 100644 index 050c47867c..0000000000 --- a/.changeset/tall-bears-taste.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Add title prop in SupportButton component diff --git a/.changeset/techdocs-a-primeira-vez.md b/.changeset/techdocs-a-primeira-vez.md deleted file mode 100644 index efc626091c..0000000000 --- a/.changeset/techdocs-a-primeira-vez.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fixes a bug that could prevent some externally hosted images (like icons or -build badges) from rendering within TechDocs documentation. diff --git a/.changeset/techdocs-metal-clouds-work.md b/.changeset/techdocs-metal-clouds-work.md deleted file mode 100644 index 64d18dcc02..0000000000 --- a/.changeset/techdocs-metal-clouds-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Adding support for user owned document filter for TechDocs custom Homepage diff --git a/.changeset/thick-donkeys-carry.md b/.changeset/thick-donkeys-carry.md deleted file mode 100644 index 41cd319654..0000000000 --- a/.changeset/thick-donkeys-carry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Migrate from the `command-exists-promise` dependency to `command-exists`. diff --git a/.changeset/thick-donkeys-fold.md b/.changeset/thick-donkeys-fold.md deleted file mode 100644 index f341724188..0000000000 --- a/.changeset/thick-donkeys-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Fix for Diagram component using hard coded namespace. diff --git a/.changeset/thirty-turkeys-sing.md b/.changeset/thirty-turkeys-sing.md deleted file mode 100644 index 07b3aaa8b7..0000000000 --- a/.changeset/thirty-turkeys-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Removed unused `typescript-json-schema` dependency. diff --git a/.changeset/tough-ravens-change.md b/.changeset/tough-ravens-change.md deleted file mode 100644 index f0567655a6..0000000000 --- a/.changeset/tough-ravens-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Use the correct parameter to create a public repository in Bitbucket Server. diff --git a/.changeset/violet-ads-change.md b/.changeset/violet-ads-change.md deleted file mode 100644 index 1de5813982..0000000000 --- a/.changeset/violet-ads-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Add support for refreshing GitLab auth sessions. diff --git a/.changeset/violet-birds-lay.md b/.changeset/violet-birds-lay.md deleted file mode 100644 index 841603058f..0000000000 --- a/.changeset/violet-birds-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-proxy-backend': patch ---- - -Bump http-proxy-middleware from 0.19.2 to 2.0.0 diff --git a/.changeset/wild-ghosts-deny.md b/.changeset/wild-ghosts-deny.md deleted file mode 100644 index efd10dbc25..0000000000 --- a/.changeset/wild-ghosts-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Export `CatalogTableRow` type diff --git a/.changeset/yellow-schools-matter.md b/.changeset/yellow-schools-matter.md deleted file mode 100644 index 15a65a7bcb..0000000000 --- a/.changeset/yellow-schools-matter.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/core': patch -'@backstage/core-components': patch ---- - -Use the Backstage `Link` component in the `Button` diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index c200104775..f6d69f050c 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,23 @@ # example-app +## 0.2.33 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/plugin-catalog@0.6.3 + - @backstage/cli@0.7.1 + - @backstage/plugin-api-docs@0.5.0 + - @backstage/plugin-jenkins@0.4.5 + - @backstage/plugin-techdocs@0.9.6 + - @backstage/plugin-circleci@0.2.16 + - @backstage/plugin-catalog-import@0.5.10 + - @backstage/plugin-sentry@0.3.12 + - @backstage/plugin-user-settings@0.2.11 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.2.32 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 1b0b3aa22c..13b90d1af5 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,19 +1,19 @@ { "name": "example-app", - "version": "0.2.32", + "version": "0.2.33", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.8.2", - "@backstage/cli": "^0.7.0", - "@backstage/core": "^0.7.12", + "@backstage/catalog-model": "^0.8.3", + "@backstage/cli": "^0.7.1", + "@backstage/core": "^0.7.13", "@backstage/integration-react": "^0.1.3", - "@backstage/plugin-api-docs": "^0.4.15", + "@backstage/plugin-api-docs": "^0.5.0", "@backstage/plugin-badges": "^0.2.2", - "@backstage/plugin-catalog": "^0.6.2", - "@backstage/plugin-catalog-import": "^0.5.9", - "@backstage/plugin-catalog-react": "^0.2.2", - "@backstage/plugin-circleci": "^0.2.15", + "@backstage/plugin-catalog": "^0.6.3", + "@backstage/plugin-catalog-import": "^0.5.10", + "@backstage/plugin-catalog-react": "^0.2.3", + "@backstage/plugin-circleci": "^0.2.16", "@backstage/plugin-cloudbuild": "^0.2.16", "@backstage/plugin-code-coverage": "^0.1.4", "@backstage/plugin-cost-insights": "^0.10.2", @@ -21,7 +21,7 @@ "@backstage/plugin-gcp-projects": "^0.2.6", "@backstage/plugin-github-actions": "^0.4.9", "@backstage/plugin-graphiql": "^0.2.11", - "@backstage/plugin-jenkins": "^0.4.4", + "@backstage/plugin-jenkins": "^0.4.5", "@backstage/plugin-kafka": "^0.2.8", "@backstage/plugin-kubernetes": "^0.4.5", "@backstage/plugin-lighthouse": "^0.2.17", @@ -31,12 +31,12 @@ "@backstage/plugin-rollbar": "^0.3.6", "@backstage/plugin-scaffolder": "^0.9.8", "@backstage/plugin-search": "^0.4.0", - "@backstage/plugin-sentry": "^0.3.11", + "@backstage/plugin-sentry": "^0.3.12", "@backstage/plugin-shortcuts": "^0.1.2", "@backstage/plugin-tech-radar": "^0.4.0", - "@backstage/plugin-techdocs": "^0.9.5", + "@backstage/plugin-techdocs": "^0.9.6", "@backstage/plugin-todo": "^0.1.2", - "@backstage/plugin-user-settings": "^0.2.10", + "@backstage/plugin-user-settings": "^0.2.11", "@backstage/theme": "^0.2.8", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 6256697036..c37137f524 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/backend-common +## 0.8.3 + +### Patch Changes + +- e5cdf0560: Provide a more clear error message when database connection fails. +- 772dbdb51: Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database + connection manager, `DatabaseManager`, which allows developers to configure database + connections on a per plugin basis. + + The `backend.database` config path allows you to set `prefix` to use an + alternate prefix for automatically generated database names, the default is + `backstage_plugin_`. Use `backend.database.plugin.` to set plugin + specific database connection configuration, e.g. + + ```yaml + backend: + database: + client: 'pg', + prefix: 'custom_prefix_' + connection: + host: 'localhost' + user: 'foo' + password: 'bar' + plugin: + catalog: + connection: + database: 'database_name_overriden' + scaffolder: + client: 'sqlite3' + connection: ':memory:' + ``` + + Migrate existing backstage installations by swapping out the database manager in the + `packages/backend/src/index.ts` file as shown below: + + ```diff + import { + - SingleConnectionDatabaseManager, + + DatabaseManager, + } from '@backstage/backend-common'; + + // ... + + function makeCreateEnv(config: Config) { + // ... + - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); + + const databaseManager = DatabaseManager.fromConfig(config); + // ... + } + ``` + +- Updated dependencies + - @backstage/config-loader@0.6.4 + ## 0.8.2 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index fc57ee59a8..04231a6a69 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.8.2", + "version": "0.8.3", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -31,7 +31,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.5", - "@backstage/config-loader": "^0.6.2", + "@backstage/config-loader": "^0.6.4", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.6", "@google-cloud/storage": "^5.8.0", @@ -76,7 +76,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/test-utils": "^0.1.12", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index c2078fd0bc..e529fe464f 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/backend-test-utils +## 0.1.3 + +### Patch Changes + +- 772dbdb51: Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database + connection manager, `DatabaseManager`, which allows developers to configure database + connections on a per plugin basis. + + The `backend.database` config path allows you to set `prefix` to use an + alternate prefix for automatically generated database names, the default is + `backstage_plugin_`. Use `backend.database.plugin.` to set plugin + specific database connection configuration, e.g. + + ```yaml + backend: + database: + client: 'pg', + prefix: 'custom_prefix_' + connection: + host: 'localhost' + user: 'foo' + password: 'bar' + plugin: + catalog: + connection: + database: 'database_name_overriden' + scaffolder: + client: 'sqlite3' + connection: ':memory:' + ``` + + Migrate existing backstage installations by swapping out the database manager in the + `packages/backend/src/index.ts` file as shown below: + + ```diff + import { + - SingleConnectionDatabaseManager, + + DatabaseManager, + } from '@backstage/backend-common'; + + // ... + + function makeCreateEnv(config: Config) { + // ... + - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); + + const databaseManager = DatabaseManager.fromConfig(config); + // ... + } + ``` + +- Updated dependencies + - @backstage/backend-common@0.8.3 + - @backstage/cli@0.7.1 + ## 0.1.2 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index c4ff5603f3..3079041c7f 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", - "@backstage/cli": "^0.7.0", + "@backstage/backend-common": "^0.8.3", + "@backstage/cli": "^0.7.1", "@backstage/config": "^0.1.5", "knex": "^0.95.1", "mysql2": "^2.2.5", @@ -41,7 +41,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "jest": "^26.0.1" }, "files": [ diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index df350ff288..3c4a2e073c 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/catalog-model +## 0.8.3 + +### Patch Changes + +- 1d2ed7844: Removed unused `typescript-json-schema` dependency. + ## 0.8.2 ### Patch Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index f9d7483dda..3477d57e71 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.8.2", + "version": "0.8.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -40,7 +40,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 8eae37ef56..32d028a32d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/cli +## 0.7.1 + +### Patch Changes + +- 3108ff7bf: Make `yarn dev` in newly created backend plugins respect the `PLUGIN_PORT` environment variable. + + You can achieve the same in your created backend plugins by making sure to properly call the port and CORS methods on your service builder. Typically in a file named `src/service/standaloneServer.ts` inside your backend plugin package, replace the following: + + ```ts + const service = createServiceBuilder(module) + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/my-plugin', router); + ``` + + With something like the following: + + ```ts + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/my-plugin', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + ``` + +- Updated dependencies + - @backstage/config-loader@0.6.4 + ## 0.7.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 29cfc1616d..4d3ed7f4df 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.7.0", + "version": "0.7.1", "private": false, "publishConfig": { "access": "public" @@ -32,7 +32,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.4.4", "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.5", - "@backstage/config-loader": "^0.6.3", + "@backstage/config-loader": "^0.6.4", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", "@lerna/project": "^4.0.0", @@ -118,9 +118,9 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/config": "^0.1.5", - "@backstage/core": "^0.7.12", + "@backstage/core": "^0.7.13", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@backstage/theme": "^0.2.8", diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 9f2ae5d214..a3b8bffd8a 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/codemods +## 0.1.2 + +### Patch Changes + +- 59752e103: Fix execution of `jscodeshift` on windows. +- Updated dependencies + - @backstage/core-components@0.1.3 + ## 0.1.1 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 2ce0172b75..aa3d358b9e 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.1", + "version": "0.1.2", "private": true, "homepage": "https://backstage.io", "repository": { diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 67ed7472b5..e0be1eebc8 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/config-loader +## 0.6.4 + +### Patch Changes + +- f00493739: Removed workaround for breaking change in typescript 4.3 and bump `typescript-json-schema` instead. This should again allow the usage of `@items.visibility ` to set the visibility of array items. + ## 0.6.3 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index a9a309428a..4a0ede9873 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.6.3", + "version": "0.6.4", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 83e5bad9f5..db49ad5adb 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/core-components +## 0.1.3 + +### Patch Changes + +- d2c31b132: Add title prop in SupportButton component +- d4644f592: Use the Backstage `Link` component in the `Button` + ## 0.1.2 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 42a98b859a..428c0e9134 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.1.2", + "version": "0.1.3", "private": false, "publishConfig": { "access": "public", @@ -71,7 +71,7 @@ }, "devDependencies": { "@backstage/core-app-api": "^0.1.2", - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index d06dbab3d5..359573ef01 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core +## 0.7.13 + +### Patch Changes + +- d4644f592: Use the Backstage `Link` component in the `Button` + ## 0.7.12 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index 4ecb3839aa..9048de576c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.7.12", + "version": "0.7.13", "private": false, "publishConfig": { "access": "public", @@ -71,7 +71,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 13f2bcb5ab..ba40e15f26 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,130 @@ # @backstage/create-app +## 1.0.0 + +### Patch Changes + +- 5db7445b4: Adding .DS_Store pattern to .gitignore in Scaffolded Backstage App. To migrate an existing app that pattern should be added manually. + + ```diff + +# macOS + +.DS_Store + ``` + +- b45e29410: This release enables the new catalog processing engine which is a major milestone for the catalog! + + This update makes processing more scalable across multiple instances, adds support for deletions and ui flagging of entities that are no longer referenced by a location. + + **Changes Required** to `catalog.ts` + + ```diff + -import { useHotCleanup } from '@backstage/backend-common'; + import { + CatalogBuilder, + - createRouter, + - runPeriodically + + createRouter + } from '@backstage/plugin-catalog-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin(env: PluginEnvironment): Promise { + - const builder = new CatalogBuilder(env); + + const builder = await CatalogBuilder.create(env); + const { + entitiesCatalog, + locationsCatalog, + - higherOrderOperation, + + locationService, + + processingEngine, + locationAnalyzer, + } = await builder.build(); + + - useHotCleanup( + - module, + - runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000), + - ); + + await processingEngine.start(); + + return await createRouter({ + entitiesCatalog, + locationsCatalog, + - higherOrderOperation, + + locationService, + locationAnalyzer, + logger: env.logger, + config: env.config, + ``` + + As this is a major internal change we have taken some precaution by still allowing the old catalog to be enabled by keeping your `catalog.ts` in it's current state. + If you encounter any issues and have to revert to the previous catalog engine make sure to raise an issue immediately as the old catalog engine is deprecated and will be removed in a future release. + +- 772dbdb51: Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database + connection manager, `DatabaseManager`, which allows developers to configure database + connections on a per plugin basis. + + The `backend.database` config path allows you to set `prefix` to use an + alternate prefix for automatically generated database names, the default is + `backstage_plugin_`. Use `backend.database.plugin.` to set plugin + specific database connection configuration, e.g. + + ```yaml + backend: + database: + client: 'pg', + prefix: 'custom_prefix_' + connection: + host: 'localhost' + user: 'foo' + password: 'bar' + plugin: + catalog: + connection: + database: 'database_name_overriden' + scaffolder: + client: 'sqlite3' + connection: ':memory:' + ``` + + Migrate existing backstage installations by swapping out the database manager in the + `packages/backend/src/index.ts` file as shown below: + + ```diff + import { + - SingleConnectionDatabaseManager, + + DatabaseManager, + } from '@backstage/backend-common'; + + // ... + + function makeCreateEnv(config: Config) { + // ... + - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); + + const databaseManager = DatabaseManager.fromConfig(config); + // ... + } + ``` + +- Updated dependencies + - @backstage/plugin-catalog@0.6.3 + - @backstage/plugin-search-backend-node@0.2.1 + - @backstage/plugin-catalog-backend@0.10.3 + - @backstage/backend-common@0.8.3 + - @backstage/cli@0.7.1 + - @backstage/plugin-api-docs@0.5.0 + - @backstage/plugin-scaffolder-backend@0.12.1 + - @backstage/plugin-techdocs@0.9.6 + - @backstage/plugin-techdocs-backend@0.8.3 + - @backstage/plugin-catalog-import@0.5.10 + - @backstage/plugin-app-backend@0.3.14 + - @backstage/plugin-proxy-backend@0.2.10 + - @backstage/plugin-rollbar-backend@0.1.12 + - @backstage/plugin-search-backend@0.2.1 + - @backstage/plugin-user-settings@0.2.11 + - @backstage/catalog-model@0.8.3 + - @backstage/plugin-auth-backend@0.3.13 + - @backstage/core@0.7.13 + ## 0.3.25 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index d5adcba510..1373c52c71 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.25", + "version": "1.0.0", "private": false, "publishConfig": { "access": "public" diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 27cd97e2e4..d51b0b0595 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-api-docs +## 0.5.0 + +### Minor Changes + +- 2ebc430c4: Rework `ApiExplorerPage` to utilize `EntityListProvider` to provide a consistent UI with the `CatalogIndexPage` which now exposes support for starring entities, pagination, and customizing columns. + +### Patch Changes + +- 14ce64b4f: Add pagination to ApiExplorerTable +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/plugin-catalog@0.6.3 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.4.15 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index bc937e2aff..417764768e 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.4.15", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ }, "dependencies": { "@asyncapi/react-component": "^0.23.0", - "@backstage/catalog-model": "^0.8.2", - "@backstage/core": "^0.7.11", - "@backstage/plugin-catalog": "^0.6.2", - "@backstage/plugin-catalog-react": "^0.2.2", + "@backstage/catalog-model": "^0.8.3", + "@backstage/core": "^0.7.13", + "@backstage/plugin-catalog": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.2.3", "@backstage/theme": "^0.2.8", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", @@ -50,7 +50,7 @@ "swagger-ui-react": "^3.37.2" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index ede16b6a5e..144d641d2c 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-app-backend +## 0.3.14 + +### Patch Changes + +- 3108ff7bf: Make `yarn dev` respect the `PLUGIN_PORT` environment variable. +- Updated dependencies + - @backstage/backend-common@0.8.3 + - @backstage/config-loader@0.6.4 + ## 0.3.13 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 4d101e7ca5..d9de7fbe27 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.13", + "version": "0.3.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", - "@backstage/config-loader": "^0.6.1", + "@backstage/backend-common": "^0.8.3", + "@backstage/config-loader": "^0.6.4", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -40,7 +40,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/supertest": "^2.0.8", "msw": "^0.20.5", "supertest": "^6.1.3" diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index d3c916fa3e..af9b4ca419 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend +## 0.3.13 + +### Patch Changes + +- 1aa31f0af: Add support for refreshing GitLab auth sessions. +- Updated dependencies + - @backstage/backend-common@0.8.3 + - @backstage/catalog-model@0.8.3 + ## 0.3.12 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index ab5e252fed..06d443c37d 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.3.12", + "version": "0.3.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/catalog-client": "^0.3.13", - "@backstage/catalog-model": "^0.8.2", + "@backstage/catalog-model": "^0.8.3", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/test-utils": "^0.1.12", @@ -68,7 +68,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index 9ceba76406..6ba2512b86 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-badges-backend +## 0.1.7 + +### Patch Changes + +- 3108ff7bf: Make `yarn dev` respect the `PLUGIN_PORT` environment variable. +- Updated dependencies + - @backstage/backend-common@0.8.3 + - @backstage/catalog-model@0.8.3 + ## 0.1.6 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 28adf1399b..99bdb25a04 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-badges-backend", - "version": "0.1.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/catalog-client": "^0.3.13", - "@backstage/catalog-model": "^0.8.2", + "@backstage/catalog-model": "^0.8.3", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@types/express": "^4.17.6", @@ -45,7 +45,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 99f78f3bd5..4de0681b34 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -34,7 +34,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index ab43d75061..454630f723 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -37,7 +37,7 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index c8ff84831a..1dc73669cd 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-catalog-backend +## 0.10.3 + +### Patch Changes + +- b45e29410: This release enables the new catalog processing engine which is a major milestone for the catalog! + + This update makes processing more scalable across multiple instances, adds support for deletions and ui flagging of entities that are no longer referenced by a location. + + **Changes Required** to `catalog.ts` + + ```diff + -import { useHotCleanup } from '@backstage/backend-common'; + import { + CatalogBuilder, + - createRouter, + - runPeriodically + + createRouter + } from '@backstage/plugin-catalog-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin(env: PluginEnvironment): Promise { + - const builder = new CatalogBuilder(env); + + const builder = await CatalogBuilder.create(env); + const { + entitiesCatalog, + locationsCatalog, + - higherOrderOperation, + + locationService, + + processingEngine, + locationAnalyzer, + } = await builder.build(); + + - useHotCleanup( + - module, + - runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000), + - ); + + await processingEngine.start(); + + return await createRouter({ + entitiesCatalog, + locationsCatalog, + - higherOrderOperation, + + locationService, + locationAnalyzer, + logger: env.logger, + config: env.config, + ``` + + As this is a major internal change we have taken some precaution by still allowing the old catalog to be enabled by keeping your `catalog.ts` in it's current state. + If you encounter any issues and have to revert to the previous catalog engine make sure to raise an issue immediately as the old catalog engine is deprecated and will be removed in a future release. + +- 72fbf4372: Switches the default catalog processing engine to use a batched streaming task execution strategy for higher parallelism. +- 18ab535c8: Rely on `SELECT ... FOR UPDATE SKIP LOCKED` where available in order to speed up processing item acquisition and reduce work duplication. +- db17fd734: Make refresh interval configurable for the `NextCatalogBuilder` using `.setRefreshIntervalSeconds()`. + + Change `DefaultProcessingDatabase` constructor to accept an options object instead of individual arguments. + +- cb09e445e: Implement `NextCatalogBuilder.addEntityProvider` +- 3108ff7bf: Make `yarn dev` respect the `PLUGIN_PORT` environment variable. +- Updated dependencies + - @backstage/plugin-search-backend-node@0.2.1 + - @backstage/backend-common@0.8.3 + - @backstage/catalog-model@0.8.3 + ## 0.10.2 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index a1238d25b6..6bc53b43da 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.10.2", + "version": "0.10.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,13 +30,13 @@ }, "dependencies": { "@azure/msal-node": "^1.0.0-beta.3", - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/catalog-client": "^0.3.13", - "@backstage/catalog-model": "^0.8.2", + "@backstage/catalog-model": "^0.8.3", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.6", - "@backstage/plugin-search-backend-node": "^0.2.0", + "@backstage/plugin-search-backend-node": "^0.2.1", "@backstage/search-common": "^0.1.2", "@microsoft/microsoft-graph-types": "^1.25.0", "@octokit/graphql": "^4.5.8", @@ -65,8 +65,8 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.2", - "@backstage/cli": "^0.7.0", + "@backstage/backend-test-utils": "^0.1.3", + "@backstage/cli": "^0.7.1", "@backstage/test-utils": "^0.1.13", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 9b6e1c4487..6cd89dbf82 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-import +## 0.5.10 + +### Patch Changes + +- 873116e5d: Fix a react warning in ``. +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.5.9 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index c017c3d08a..70c3f79b21 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.5.9", + "version": "0.5.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.8.2", + "@backstage/catalog-model": "^0.8.3", "@backstage/catalog-client": "^0.3.13", - "@backstage/core": "^0.7.11", + "@backstage/core": "^0.7.13", "@backstage/integration": "^0.5.6", "@backstage/integration-react": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.2.3", "@backstage/theme": "^0.2.8", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -53,7 +53,7 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 88d7a04a78..c532a11ac7 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-react +## 0.2.3 + +### Patch Changes + +- 172c97324: Add `EntityLifecyclePicker` and `EntityOwnerPicker` UI components to allow filtering by `spec.lifecycle` and `spec.owner` on catalog-related pages. +- Updated dependencies + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.2.2 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index bb814d7d4f..c5a871620b 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ }, "dependencies": { "@backstage/catalog-client": "^0.3.13", - "@backstage/catalog-model": "^0.8.2", - "@backstage/core": "^0.7.12", + "@backstage/catalog-model": "^0.8.3", + "@backstage/core": "^0.7.13", "@backstage/core-plugin-api": "^0.1.2", "@backstage/integration": "^0.5.6", "@material-ui/core": "^4.11.0", @@ -44,8 +44,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", - "@backstage/core": "^0.7.12", + "@backstage/cli": "^0.7.1", + "@backstage/core": "^0.7.13", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 53df1b37f8..e0ccbd068c 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog +## 0.6.3 + +### Patch Changes + +- 30c2fdad2: Exports `CatalogLayout` and `CreateComponentButton` for catalog customization. +- e2d68f1ce: Truncate long entity names on the system diagram +- d2d42a7fa: Fix for Diagram component using hard coded namespace. +- 2ebc430c4: Export `CatalogTableRow` type +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.6.2 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index ce72d95103..4e35cc7618 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.6.2", + "version": "0.6.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,12 +31,12 @@ }, "dependencies": { "@backstage/catalog-client": "^0.3.13", - "@backstage/catalog-model": "^0.8.2", - "@backstage/core": "^0.7.12", + "@backstage/catalog-model": "^0.8.3", + "@backstage/core": "^0.7.13", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.6", "@backstage/integration-react": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.2.3", "@backstage/theme": "^0.2.8", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -53,7 +53,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 09abbc3bd1..dcbf6ad2b1 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-circleci +## 0.2.16 + +### Patch Changes + +- 2ec118cc2: Remove moment as part 1 of migration to `lexon` +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.2.15 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index a7cd5f6b84..90e8b71174 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.2.15", + "version": "0.2.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.8.2", - "@backstage/core": "^0.7.11", - "@backstage/plugin-catalog-react": "^0.2.2", + "@backstage/catalog-model": "^0.8.3", + "@backstage/core": "^0.7.13", + "@backstage/plugin-catalog-react": "^0.2.3", "@backstage/theme": "^0.2.8", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -49,7 +49,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 29cc77d7e9..ffd765fbcd 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -47,7 +47,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 7d1027f84f..11e341abed 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-code-coverage-backend +## 0.1.7 + +### Patch Changes + +- 3108ff7bf: Make `yarn dev` respect the `PLUGIN_PORT` environment variable. +- Updated dependencies + - @backstage/backend-common@0.8.3 + - @backstage/catalog-model@0.8.3 + ## 0.1.6 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 7def52f498..e3aab9bd38 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-coverage-backend", - "version": "0.1.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,9 +19,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/catalog-client": "^0.3.13", - "@backstage/catalog-model": "^0.8.2", + "@backstage/catalog-model": "^0.8.3", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.6", @@ -36,7 +36,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.21.2", diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 51626acef0..9d963ee50b 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -39,7 +39,7 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 427de0967b..50749bb1a7 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -34,7 +34,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 32c985be24..5ffb4c6f49 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -54,7 +54,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index fd6ff19a93..746d8fc1ce 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -45,7 +45,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 9f00172358..e45f0f2789 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -47,7 +47,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 3be5ae031b..6eab528ff6 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -41,7 +41,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index e90905a767..3a492d7562 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -36,7 +36,7 @@ "react": "^16.13.1" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 9e651194db..ebbf82ccce 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -50,7 +50,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index af49b876d3..cc98232b2d 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -37,7 +37,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 66d136b6f8..8171e2ae83 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -42,7 +42,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 209d105b4c..4a8f7ec366 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -44,7 +44,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 84ebdb096e..cb7fec1721 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -37,7 +37,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index a42bd24430..9fbe7959ff 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-jenkins +## 0.4.5 + +### Patch Changes + +- b861c082b: Support showing build details for branches with slashes in their names +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.4.4 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index a2204ac704..bf65ed381c 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.4.4", + "version": "0.4.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.8.2", - "@backstage/core": "^0.7.11", - "@backstage/plugin-catalog-react": "^0.2.2", + "@backstage/catalog-model": "^0.8.3", + "@backstage/core": "^0.7.13", + "@backstage/plugin-catalog-react": "^0.2.3", "@backstage/theme": "^0.2.8", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,7 +47,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 01e9408633..4cfe3f1e0b 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -33,7 +33,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 96820e2a4f..a370335c20 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -49,7 +49,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index f053479f6d..6a2148b00b 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -46,7 +46,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index c3abf86f6b..6a26dccca1 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -41,7 +41,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/org/package.json b/plugins/org/package.json index 2266eab7fb..0314bf2e03 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -35,7 +35,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 790866aa23..8d07d8b1de 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -46,7 +46,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 2147e0682a..cdf55997d0 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-proxy-backend +## 0.2.10 + +### Patch Changes + +- 3108ff7bf: Make `yarn dev` respect the `PLUGIN_PORT` environment variable. +- 6ffcf9ed8: Bump http-proxy-middleware from 0.19.2 to 2.0.0 +- Updated dependencies + - @backstage/backend-common@0.8.3 + ## 0.2.9 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 5e8549f6c7..e3a1849231 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.2.9", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,7 +28,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -42,7 +42,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index a162c650b3..e7c10681ba 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -45,7 +45,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 0318d3a111..7f4a7e1b8e 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-rollbar-backend +## 0.1.12 + +### Patch Changes + +- 3108ff7bf: Make `yarn dev` respect the `PLUGIN_PORT` environment variable. +- Updated dependencies + - @backstage/backend-common@0.8.3 + ## 0.1.11 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index ca81bc27f2..e14f6f5a6c 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar-backend", - "version": "0.1.11", + "version": "0.1.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", "axios": "^0.21.1", @@ -47,7 +47,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 4f55abda2b..e0e5011a59 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -47,7 +47,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index c0cfe8dc58..4ea79dfeed 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-scaffolder-backend +## 0.12.1 + +### Patch Changes + +- 55a834f3c: Use the correct parameter to create a public repository in Bitbucket Server for the v2 templates +- 745351190: Describe `publish:github` scaffolder action fields + + This change adds a description to the fields with examples of what to input. The + `collaborators` description is also expanded a bit to make it more clear that + these are additional compared to access and owner. + +- 090dfe65d: Adds support to enable LFS for hosted Bitbucket +- 878c1851d: Add a `topics` input to `publish:github` action that can be used to set topics on the repository upon creation. +- 4ca322826: Migrate from the `command-exists-promise` dependency to `command-exists`. +- df3ac03cf: Use the correct parameter to create a public repository in Bitbucket Server. +- Updated dependencies + - @backstage/backend-common@0.8.3 + - @backstage/catalog-model@0.8.3 + ## 0.12.0 ### Minor Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 9c782d7053..b9737bc2fc 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.12.0", + "version": "0.12.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/catalog-client": "^0.3.13", - "@backstage/catalog-model": "^0.8.2", + "@backstage/catalog-model": "^0.8.3", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.6", @@ -64,7 +64,7 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/test-utils": "^0.1.13", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index a7541a5ddc..69d71e7966 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -61,7 +61,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 395074f659..a0e81d81ba 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-search-backend-node +## 0.2.1 + +### Patch Changes + +- 14aad6113: Improved the quality of free text searches in LunrSearchEngine. + ## 0.2.0 ### Minor Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 3f0cf13f8f..fbbe02778f 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-node", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,8 +25,8 @@ "@types/lunr": "^2.3.3" }, "devDependencies": { - "@backstage/backend-common": "^0.8.2", - "@backstage/cli": "^0.7.0" + "@backstage/backend-common": "^0.8.3", + "@backstage/cli": "^0.7.1" }, "files": [ "dist" diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 491a1da774..47188017dc 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search-backend +## 0.2.1 + +### Patch Changes + +- 3108ff7bf: Make `yarn dev` respect the `PLUGIN_PORT` environment variable. +- Updated dependencies + - @backstage/plugin-search-backend-node@0.2.1 + - @backstage/backend-common@0.8.3 + ## 0.2.0 ### Minor Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 8b906ea9c2..48729e88d9 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,9 +19,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/search-common": "^0.1.2", - "@backstage/plugin-search-backend-node": "^0.2.0", + "@backstage/plugin-search-backend-node": "^0.2.1", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -29,7 +29,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search/package.json b/plugins/search/package.json index 9d6c81272c..5c84748e06 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -47,7 +47,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 2ee6f107a2..02e90ce9a7 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.3.12 + +### Patch Changes + +- 6c12f0ec6: Migrated the package from `timeago.js` to `luxon`. See #4278 +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.3.11 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 72404009e2..0aea331aba 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.3.11", + "version": "0.3.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.8.2", - "@backstage/core": "^0.7.11", - "@backstage/plugin-catalog-react": "^0.2.2", + "@backstage/catalog-model": "^0.8.3", + "@backstage/core": "^0.7.13", + "@backstage/plugin-catalog-react": "^0.2.3", "@backstage/theme": "^0.2.8", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -46,7 +46,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 7614c2aedb..cd5384c215 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -35,7 +35,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 2f986f7e53..115d8e1f61 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -47,7 +47,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index f97ecd0f61..dead1f80a1 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-splunk-on-call +## 0.3.2 + +### Patch Changes + +- ae903f8e7: Added config schema to expose `splunkOnCall.eventsRestEndpoint` config option to the frontend +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.3.1 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 740418e258..714283d5b4 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-splunk-on-call", - "version": "0.3.1", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.8.2", - "@backstage/core": "^0.7.11", - "@backstage/plugin-catalog-react": "^0.2.2", + "@backstage/catalog-model": "^0.8.3", + "@backstage/core": "^0.7.13", + "@backstage/plugin-catalog-react": "^0.2.3", "@backstage/theme": "^0.2.8", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -45,7 +45,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 82f749a03e..efc99d8890 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -43,7 +43,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 9aed4a1781..1169655028 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-techdocs-backend +## 0.8.3 + +### Patch Changes + +- 6013a16dc: TechDocs: Support configurable working directory as temp dir +- 3108ff7bf: Make `yarn dev` respect the `PLUGIN_PORT` environment variable. +- Updated dependencies + - @backstage/backend-common@0.8.3 + - @backstage/catalog-model@0.8.3 + ## 0.8.2 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index e6f7e00109..9284ac9ac8 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.8.2", + "version": "0.8.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", - "@backstage/catalog-model": "^0.8.2", + "@backstage/backend-common": "^0.8.3", + "@backstage/catalog-model": "^0.8.3", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/techdocs-common": "^0.6.3", @@ -45,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/dockerode": "^3.2.1", "supertest": "^6.1.3" }, diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 103ec1c5d7..c6d61c411b 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-techdocs +## 0.9.6 + +### Patch Changes + +- 938aee2fb: Fix the link to the documentation page when no owned documents are displayed +- 2e1fbe203: Do not add trailing slash for .html pages during doc links rewriting +- 9b57fda8b: Fixes a bug that could prevent some externally hosted images (like icons or + build badges) from rendering within TechDocs documentation. +- 667656c8b: Adding support for user owned document filter for TechDocs custom Homepage +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + ## 0.9.5 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 76bcfd8b0d..01b412b501 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.9.5", + "version": "0.9.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/catalog-model": "^0.8.2", - "@backstage/core": "^0.7.12", + "@backstage/catalog-model": "^0.8.3", + "@backstage/core": "^0.7.13", "@backstage/integration": "^0.5.6", "@backstage/integration-react": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.2.2", + "@backstage/plugin-catalog-react": "^0.2.3", "@backstage/theme": "^0.2.8", "@backstage/errors": "^0.1.1", "@material-ui/core": "^4.11.0", @@ -51,7 +51,7 @@ "sanitize-html": "^2.3.2" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index 718bc62abf..a295ad777c 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-todo-backend +## 0.1.7 + +### Patch Changes + +- a6a0ba7ff: Bump `leasot` dependency from 11.5.0 to 12.0.0, removing support for Node.js version 10. +- Updated dependencies + - @backstage/backend-common@0.8.3 + - @backstage/catalog-model@0.8.3 + ## 0.1.6 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 309846c46f..d08dc3dcae 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-todo-backend", - "version": "0.1.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,9 +24,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.2", + "@backstage/backend-common": "^0.8.3", "@backstage/catalog-client": "^0.3.13", - "@backstage/catalog-model": "^0.8.2", + "@backstage/catalog-model": "^0.8.3", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.6", @@ -39,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@types/supertest": "^2.0.8", "msw": "^0.21.2", "supertest": "^6.1.3" diff --git a/plugins/todo/package.json b/plugins/todo/package.json index f003bfa8e9..035efb6eac 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -40,7 +40,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/core-app-api": "^0.1.2", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 5b1706e0c7..1c0dbb2821 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-user-settings +## 0.2.11 + +### Patch Changes + +- 42a2d2ebc: Fix a bug that prevented changing themes on the user settings page when the theme `id` didn't match exactly the theme `variant`. +- Updated dependencies + - @backstage/core@0.7.13 + ## 0.2.10 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index ad6e42c398..bdf1a0026c 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.2.10", + "version": "0.2.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.11", + "@backstage/core": "^0.7.13", "@backstage/theme": "^0.2.8", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,7 +41,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/core-plugin-api": "^0.1.2", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index bb0c56b198..a5c00bfc9e 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -41,7 +41,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/dev-utils": "^0.1.17", "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", From fea7fa0ba6e0d38e4a08747426fb6a7509912159 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Wed, 9 Jun 2021 10:22:56 +0200 Subject: [PATCH 173/206] Return a `304 Not Modified` from the `/sync/:namespace/:kind/:name` endpoint if nothing was built Signed-off-by: Dominik Henneke --- .changeset/techdocs-poor-forks-repeat.md | 5 +++++ plugins/techdocs-backend/src/DocsBuilder/builder.ts | 10 ++++++++-- plugins/techdocs-backend/src/service/router.ts | 13 ++++++++++--- 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 .changeset/techdocs-poor-forks-repeat.md diff --git a/.changeset/techdocs-poor-forks-repeat.md b/.changeset/techdocs-poor-forks-repeat.md new file mode 100644 index 0000000000..89c4e0dc5a --- /dev/null +++ b/.changeset/techdocs-poor-forks-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Return a `304 Not Modified` from the `/sync/:namespace/:kind/:name` endpoint if nothing was built. This enables the caller to know whether a refresh of the docs page will return updated content (-> `201 Created`) or not (-> `304 Not Modified`). diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index dce391ff3f..24549f8b6f 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -68,7 +68,11 @@ export class DocsBuilder { this.config = config; } - public async build(): Promise { + /** + * Build the docs and return whether they have been newly generated or have been cached + * @returns true, if the docs have been built. false, if the cached docs are still up-to-date. + */ + public async build(): Promise { if (!this.entity.metadata.uid) { throw new Error( 'Trying to build documentation for entity not in service catalog', @@ -125,7 +129,7 @@ export class DocsBuilder { this.entity, )} are unmodified. Using cache, skipping generate and prepare`, ); - return; + return false; } throw new Error(err.message); } @@ -205,5 +209,7 @@ export class DocsBuilder { // Update the last check time for the entity new BuildMetadataStorage(this.entity.metadata.uid).setLastUpdated(); + + return true; } } diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 1cd4075da0..1a8bdb9c8f 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -16,7 +16,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { NotFoundError } from '@backstage/errors'; +import { NotFoundError, NotModifiedError } from '@backstage/errors'; import { GeneratorBuilder, getLocationForEntity, @@ -172,10 +172,10 @@ export async function createRouter({ case 'awsS3': case 'azureBlobStorage': case 'openStackSwift': - case 'googleGcs': + case 'googleGcs': { // This block should be valid for all storage implementations. So no need to duplicate in future, // add the publisher type in the list here. - await docsBuilder.build(); + const updated = await docsBuilder.build(); // With a maximum of ~5 seconds wait, check if the files got published and if docs will be fetched // on the user's page. If not, respond with a message asking them to check back later. // The delay here is to make sure GCS/AWS/etc. registers newly uploaded files which is usually <1 second @@ -194,10 +194,17 @@ export async function createRouter({ 'Sorry! It took too long for the generated docs to show up in storage. Check back later.', ); } + + if (!updated) { + throw new NotModifiedError(); + } + res .status(201) .json({ message: 'Docs updated or did not need updating' }); break; + } + default: throw new NotFoundError( `Publisher type ${publisherType} is not supported by techdocs-backend docs builder.`, From 1dfec7a2ae788d8b9c18a2ad278c11fa58c6bf5c Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Wed, 9 Jun 2021 10:49:10 +0200 Subject: [PATCH 174/206] Refactor the implicit logic from `` into an explicit state machine Signed-off-by: Dominik Henneke --- .changeset/techdocs-cool-rivers-suffer.md | 5 + plugins/techdocs/dev/api.ts | 149 ------ plugins/techdocs/dev/index.tsx | 193 +++++++- plugins/techdocs/package.json | 1 + plugins/techdocs/src/api.ts | 4 +- plugins/techdocs/src/client.ts | 14 +- .../techdocs/src/reader/components/Reader.tsx | 155 +++--- .../reader/components/useReaderState.test.tsx | 459 ++++++++++++++++++ .../src/reader/components/useReaderState.ts | 335 +++++++++++++ .../reader/transformers/addBaseUrl.test.ts | 2 +- 10 files changed, 1053 insertions(+), 264 deletions(-) create mode 100644 .changeset/techdocs-cool-rivers-suffer.md delete mode 100644 plugins/techdocs/dev/api.ts create mode 100644 plugins/techdocs/src/reader/components/useReaderState.test.tsx create mode 100644 plugins/techdocs/src/reader/components/useReaderState.ts diff --git a/.changeset/techdocs-cool-rivers-suffer.md b/.changeset/techdocs-cool-rivers-suffer.md new file mode 100644 index 0000000000..2386d27525 --- /dev/null +++ b/.changeset/techdocs-cool-rivers-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Refactor the implicit logic from `` into an explicit state machine. This resolves some state synchronization issues when content is refreshed or rebuilt in the backend. diff --git a/plugins/techdocs/dev/api.ts b/plugins/techdocs/dev/api.ts deleted file mode 100644 index e764bf82bb..0000000000 --- a/plugins/techdocs/dev/api.ts +++ /dev/null @@ -1,149 +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 { EntityName } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; -import { DiscoveryApi, IdentityApi } from '@backstage/core'; -import { NotFoundError } from '@backstage/errors'; -import { TechDocsStorageApi } from '../src/api'; - -export class TechDocsDevStorageApi implements TechDocsStorageApi { - public configApi: Config; - public discoveryApi: DiscoveryApi; - public identityApi: IdentityApi; - - constructor({ - configApi, - discoveryApi, - identityApi, - }: { - configApi: Config; - discoveryApi: DiscoveryApi; - identityApi: IdentityApi; - }) { - this.configApi = configApi; - this.discoveryApi = discoveryApi; - this.identityApi = identityApi; - } - - async getApiOrigin() { - return ( - this.configApi.getOptionalString('techdocs.requestUrl') ?? - (await this.discoveryApi.getBaseUrl('techdocs')) - ); - } - - async getStorageUrl() { - return ( - this.configApi.getOptionalString('techdocs.storageUrl') ?? - `${await this.discoveryApi.getBaseUrl('techdocs')}/static/docs` - ); - } - - async getBuilder() { - return this.configApi.getString('techdocs.builder'); - } - - async fetchUrl(url: string) { - const token = await this.identityApi.getIdToken(); - return fetch(url, { - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }); - } - - async getEntityDocs(entityId: EntityName, path: string) { - const { kind, namespace, name } = entityId; - - const storageUrl = await this.getStorageUrl(); - const url = `${storageUrl}/${namespace}/${kind}/${name}/${path}`; - const token = await this.identityApi.getIdToken(); - - const request = await fetch( - `${url.endsWith('/') ? url : `${url}/`}index.html`, - { - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }, - ); - - let errorMessage = ''; - switch (request.status) { - case 404: - errorMessage = 'Page not found. '; - // path is empty for the home page of an entity's docs site - if (!path) { - errorMessage += - 'This could be because there is no index.md file in the root of the docs directory of this repository.'; - } - throw new NotFoundError(errorMessage); - case 500: - errorMessage = - 'Could not generate documentation or an error in the TechDocs backend. '; - throw new Error(errorMessage); - default: - // Do nothing - break; - } - - return request.text(); - } - - /** - * Check if docs are the latest version and trigger rebuilds if not - * - * @param {EntityName} entityId Object containing entity data like name, namespace, etc. - * @returns {boolean} Whether documents are currently synchronized to newest version - * @throws {Error} Throws error on error from sync endpoint - */ - async syncEntityDocs(entityId: EntityName) { - const { kind, namespace, name } = entityId; - - const apiOrigin = await this.getApiOrigin(); - const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`; - let request; - let attempts: number = 0; - // retry if request times out, up to 5 times - // can happen due to docs taking too long to generate - while (!request || (request.status === 408 && attempts < 5)) { - attempts++; - request = await this.fetchUrl( - `${url.endsWith('/') ? url : `${url}/`}index.html`, - ); - } - - switch (request.status) { - case 404: - throw (await request.json()).error; - case 200: - case 201: - return true; - // for timeout and misc errors, handle without error to allow viewing older docs - // if older docs not available, - // Reader will show 404 error coming from getEntityDocs - case 408: - default: - return false; - } - } - - async getBaseUrl( - oldBaseUrl: string, - entityId: EntityName, - path: string, - ): Promise { - const { name } = entityId; - const apiOrigin = await this.getApiOrigin(); - return new URL(oldBaseUrl, `${apiOrigin}/${name}/${path}`).toString(); - } -} diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 3eefa0a814..a778161b35 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -14,11 +14,105 @@ * limitations under the License. */ -import { configApiRef, discoveryApiRef, identityApiRef } from '@backstage/core'; +import { + configApiRef, + discoveryApiRef, + Header, + identityApiRef, + Page, + TabbedLayout, +} from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; -import { techdocsPlugin } from '../src/plugin'; -import { TechDocsDevStorageApi } from './api'; -import { techdocsStorageApiRef } from '../src'; +import { NotFoundError } from '@backstage/errors'; +import React from 'react'; +import { + Reader, + SyncResult, + TechDocsStorageApi, + techdocsStorageApiRef, +} from '../src'; + +// used so each route can provide it's own implementation in the constructor of the react component +let apiHolder: TechDocsStorageApi | undefined = undefined; + +const apiBridge: TechDocsStorageApi = { + getApiOrigin: async () => '', + getBaseUrl: (...args) => apiHolder!.getBaseUrl(...args), + getBuilder: () => apiHolder!.getBuilder(), + getStorageUrl: () => apiHolder!.getStorageUrl(), + getEntityDocs: (...args) => apiHolder!.getEntityDocs(...args), + syncEntityDocs: (...args) => apiHolder!.syncEntityDocs(...args), +}; + +const mockContent = ` +

Hello World!

+

This is an example content that will actually be provided by a MkDocs powered site

+`; + +function createPage({ + entityDocs, + syncDocs, + syncDocsDelay, +}: { + entityDocs?: (props: { + called: number; + content: string; + }) => string | Promise; + syncDocs: () => SyncResult; + syncDocsDelay?: number; +}) { + class Api implements TechDocsStorageApi { + private entityDocsCallCount: number = 0; + + getApiOrigin = async () => ''; + getBaseUrl = async () => ''; + getBuilder = async () => 'local'; + getStorageUrl = async () => ''; + + async getEntityDocs() { + await new Promise(resolve => setTimeout(resolve, 500)); + + if (!entityDocs) { + return mockContent; + } + + return entityDocs({ + called: this.entityDocsCallCount++, + content: mockContent, + }); + } + + async syncEntityDocs() { + if (syncDocsDelay) { + await new Promise(resolve => setTimeout(resolve, syncDocsDelay)); + } + + return syncDocs(); + } + } + + class Component extends React.Component { + constructor(props: {}) { + super(props); + + apiHolder = new Api(); + } + + render() { + return ( + + ); + } + } + + return ; +} createDevApp() .registerApi({ @@ -28,12 +122,89 @@ createDevApp() discoveryApi: discoveryApiRef, identityApi: identityApiRef, }, - factory: ({ configApi, discoveryApi, identityApi }) => - new TechDocsDevStorageApi({ - configApi, - discoveryApi, - identityApi, - }), + factory: () => apiBridge, + }) + + .addPage({ + title: 'TechDocs', + element: ( + +
+ + + {createPage({ + syncDocs: () => 'cached', + })} + + + + {createPage({ + syncDocs: () => 'updated', + syncDocsDelay: 2000, + })} + + + + {createPage({ + entityDocs: ({ called, content }) => { + if (called < 1) { + throw new NotFoundError(); + } + + return content; + }, + syncDocs: () => 'updated', + syncDocsDelay: 10000, + })} + + + + {createPage({ + entityDocs: () => { + throw new NotFoundError('Not found, some error message...'); + }, + syncDocs: () => 'cached', + })} + + + + {createPage({ + entityDocs: () => { + throw new Error('Another more critical error'); + }, + syncDocs: () => 'cached', + })} + + + + {createPage({ + syncDocs: () => { + throw new Error('Some random error'); + }, + syncDocsDelay: 2000, + })} + + + + {createPage({ + entityDocs: () => { + throw new Error('Some random error'); + }, + syncDocs: () => { + throw new Error('Some random error'); + }, + syncDocsDelay: 2000, + })} + + + + {createPage({ + syncDocs: () => 'timeout', + syncDocsDelay: 2000, + })} + + + + ), }) - .registerPlugin(techdocsPlugin) .render(); diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 76bcfd8b0d..c3336ac3b2 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -56,6 +56,7 @@ "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", + "@testing-library/react-hooks": "^3.4.2", "@testing-library/user-event": "^13.1.8", "@types/react": "^16.9", "@types/jest": "^26.0.7", diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 204cc1b5a8..b9c8725ce9 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -28,12 +28,14 @@ export const techdocsApiRef = createApiRef({ description: 'Used to make requests towards techdocs API', }); +export type SyncResult = 'cached' | 'updated' | 'timeout'; + export interface TechDocsStorageApi { getApiOrigin(): Promise; getStorageUrl(): Promise; getBuilder(): Promise; getEntityDocs(entityId: EntityName, path: string): Promise; - syncEntityDocs(entityId: EntityName): Promise; + syncEntityDocs(entityId: EntityName): Promise; getBaseUrl( oldBaseUrl: string, entityId: EntityName, diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 245cfb0154..16617dcbce 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -18,7 +18,7 @@ import { EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { DiscoveryApi, IdentityApi } from '@backstage/core'; import { NotFoundError } from '@backstage/errors'; -import { TechDocsApi, TechDocsStorageApi } from './api'; +import { SyncResult, TechDocsApi, TechDocsStorageApi } from './api'; import { TechDocsEntityMetadata, TechDocsMetadata } from './types'; /** @@ -195,7 +195,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { * @returns {boolean} Whether documents are currently synchronized to newest version * @throws {Error} Throws error on error from sync endpoint in Techdocs Backend */ - async syncEntityDocs(entityId: EntityName): Promise { + async syncEntityDocs(entityId: EntityName): Promise { const { kind, namespace, name } = entityId; const apiOrigin = await this.getApiOrigin(); @@ -215,16 +215,20 @@ export class TechDocsStorageClient implements TechDocsStorageApi { switch (request.status) { case 404: throw new NotFoundError((await request.json()).error); + case 200: - case 201: case 304: - return true; + return 'cached'; + + case 201: + return 'updated'; + // for timeout and misc errors, handle without error to allow viewing older docs // if older docs not available, // Reader will show 404 error coming from getEntityDocs case 408: default: - return false; + return 'timeout'; } } diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index c779985364..75dade20fa 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { EntityName } from '@backstage/catalog-model'; import { useApi } from '@backstage/core'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { BackstageTheme } from '@backstage/theme'; import { useTheme } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { useAsync } from 'react-use'; import { techdocsStorageApiRef } from '../../api'; import { addBaseUrl, @@ -37,7 +37,7 @@ import { } from '../transformers'; import { TechDocsNotFound } from './TechDocsNotFound'; import TechDocsProgressBar from './TechDocsProgressBar'; -import { useRawPage } from './useRawPage'; +import { useReaderState } from './useReaderState'; type Props = { entityId: EntityName; @@ -49,61 +49,37 @@ export const Reader = ({ entityId, onReady }: Props) => { const { '*': path } = useParams(); const theme = useTheme(); + const { state, content: rawPage, errorMessage } = useReaderState( + kind, + namespace, + name, + path, + ); + const techdocsStorageApi = useApi(techdocsStorageApiRef); const [sidebars, setSidebars] = useState(); const navigate = useNavigate(); const shadowDomRef = useRef(null); - const [loadedPath, setLoadedPath] = useState(''); - const [atInitialLoad, setAtInitialLoad] = useState(true); - const [newerDocsExist, setNewerDocsExist] = useState(false); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); - const { - value: isSynced, - loading: syncInProgress, - error: syncError, - } = useAsync(async () => { - // Attempt to sync only if `techdocs.builder` in app config is set to 'local' - if ((await techdocsStorageApi.getBuilder()) !== 'local') { - return Promise.resolve({ - value: true, - loading: null, - error: null, + const updateSidebarPosition = useCallback(() => { + if (!!shadowDomRef.current && !!sidebars) { + const mdTabs = shadowDomRef.current!.querySelector( + '.md-container > .md-tabs', + ); + sidebars!.forEach(sidebar => { + const newTop = Math.max( + shadowDomRef.current!.getBoundingClientRect().top, + 0, + ); + sidebar.style.top = mdTabs + ? `${newTop + mdTabs.getBoundingClientRect().height}px` + : `${newTop}px`; }); } - return techdocsStorageApi.syncEntityDocs({ kind, namespace, name }); - }, [techdocsStorageApi, kind, namespace, name]); - - const { - value: rawPage, - loading: docLoading, - error: docLoadError, - retry, - } = useRawPage(path, kind, namespace, name); + }, [shadowDomRef, sidebars]); useEffect(() => { - if (isSynced && newerDocsExist && path !== loadedPath) { - retry(); - } - }); - - useEffect(() => { - const updateSidebarPosition = () => { - if (!!shadowDomRef.current && !!sidebars) { - const mdTabs = shadowDomRef.current!.querySelector( - '.md-container > .md-tabs', - ); - sidebars!.forEach(sidebar => { - const newTop = Math.max( - shadowDomRef.current!.getBoundingClientRect().top, - 0, - ); - sidebar.style.top = mdTabs - ? `${newTop + mdTabs.getBoundingClientRect().height}px` - : `${newTop}px`; - }); - } - }; updateSidebarPosition(); window.addEventListener('scroll', updateSidebarPosition); window.addEventListener('resize', updateSidebarPosition); @@ -111,28 +87,8 @@ export const Reader = ({ entityId, onReady }: Props) => { window.removeEventListener('scroll', updateSidebarPosition); window.removeEventListener('resize', updateSidebarPosition); }; - }, [shadowDomRef, sidebars]); - - useEffect(() => { - if (rawPage) { - setLoadedPath(path); - } - }, [rawPage, path]); - - useEffect(() => { - if (atInitialLoad === false) { - return; - } - setTimeout(() => { - setAtInitialLoad(false); - }, 5000); - }); - - useEffect(() => { - if (!atInitialLoad && !!rawPage && syncInProgress) { - setNewerDocsExist(true); - } - }, [atInitialLoad, rawPage, syncInProgress]); + // an update to "state" might lead to an updated UI so we include it as a trigger + }, [updateSidebarPosition, state]); useEffect(() => { if (!rawPage || !shadowDomRef.current) { @@ -142,12 +98,16 @@ export const Reader = ({ entityId, onReady }: Props) => { onReady(); } // Pre-render - const transformedElement = transformer(rawPage.content, [ + const transformedElement = transformer(rawPage, [ sanitizeDOM(), addBaseUrl({ techdocsStorageApi, - entityId: rawPage.entityId, - path: rawPage.path, + entityId: { + kind, + name, + namespace, + }, + path, }), rewriteDocLinks(), removeMkdocsHeader(), @@ -292,10 +252,6 @@ export const Reader = ({ entityId, onReady }: Props) => { baseUrl: window.location.origin, onClick: (_: MouseEvent, url: string) => { const parsedUrl = new URL(url); - if (newerDocsExist && isSynced) { - // link navigation will load newer docs - setNewerDocsExist(false); - } if (parsedUrl.hash) { navigate(`${parsedUrl.pathname}${parsedUrl.hash}`); @@ -337,6 +293,10 @@ export const Reader = ({ entityId, onReady }: Props) => { }), ]); }, [ + path, + kind, + namespace, + name, rawPage, navigate, onReady, @@ -347,39 +307,40 @@ export const Reader = ({ entityId, onReady }: Props) => { theme.palette.primary.main, theme.palette.background.paper, theme.palette.background.default, - newerDocsExist, - isSynced, scmIntegrationsApi, ]); - // docLoadError not considered an error state if sync request is still ongoing - // or sync just completed and doc is loading again - if ((docLoadError && !syncInProgress && !docLoading) || syncError) { - let errMessage = ''; - if (docLoadError) { - errMessage += ` Load error: ${docLoadError}`; - } - if (syncError) errMessage += ` Build error: ${syncError}`; - return ; - } - return ( <> - {newerDocsExist && !isSynced ? ( + {(state === 'CHECKING' || state === 'INITIAL_BUILD') && ( + + )} + {state === 'CONTENT_STALE_REFRESHING' && ( A newer version of this documentation is being prepared and will be available shortly. - ) : null} - {newerDocsExist && isSynced ? ( + )} + {state === 'CONTENT_STALE_READY' && ( A newer version of this documentation is now available, please refresh to view. - ) : null} - {docLoading || (docLoadError && syncInProgress) ? ( - - ) : null} + )} + {state === 'CONTENT_STALE_TIMEOUT' && ( + + Building a newer version of this documentation took longer than + expected. Please refresh to try again. + + )} + {state === 'CONTENT_STALE_ERROR' && ( + + Building a newer version of this documentation failed. {errorMessage} + + )} + {state === 'CONTENT_NOT_FOUND' && ( + + )}
); diff --git a/plugins/techdocs/src/reader/components/useReaderState.test.tsx b/plugins/techdocs/src/reader/components/useReaderState.test.tsx new file mode 100644 index 0000000000..d5579ddd5a --- /dev/null +++ b/plugins/techdocs/src/reader/components/useReaderState.test.tsx @@ -0,0 +1,459 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { NotFoundError } from '@backstage/errors'; +import { act, renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { techdocsStorageApiRef } from '../../api'; +import { + calculateDisplayState, + reducer, + useReaderState, +} from './useReaderState'; + +describe('useReaderState', () => { + let Wrapper: React.ComponentType; + + const techdocsStorageApi: jest.Mocked = { + getApiOrigin: jest.fn(), + getBaseUrl: jest.fn(), + getBuilder: jest.fn(), + getEntityDocs: jest.fn(), + getStorageUrl: jest.fn(), + syncEntityDocs: jest.fn(), + }; + + beforeEach(() => { + const apis = ApiRegistry.with(techdocsStorageApiRef, techdocsStorageApi); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + describe('calculateDisplayState', () => { + it.each` + contentLoading | content | activeSyncState | expected + ${true} | ${''} | ${''} | ${'CHECKING'} + ${false} | ${undefined} | ${'CHECKING'} | ${'CHECKING'} + ${false} | ${undefined} | ${'BUILDING'} | ${'INITIAL_BUILD'} + ${false} | ${undefined} | ${'BUILD_READY'} | ${'CONTENT_NOT_FOUND'} + ${false} | ${undefined} | ${'BUILD_TIMED_OUT'} | ${'CONTENT_NOT_FOUND'} + ${false} | ${undefined} | ${'UP_TO_DATE'} | ${'CONTENT_NOT_FOUND'} + ${false} | ${undefined} | ${'ERROR'} | ${'CONTENT_NOT_FOUND'} + ${false} | ${'asdf'} | ${'CHECKING'} | ${'CONTENT_FRESH'} + ${false} | ${'asdf'} | ${'BUILDING'} | ${'CONTENT_STALE_REFRESHING'} + ${false} | ${'asdf'} | ${'BUILD_READY'} | ${'CONTENT_STALE_READY'} + ${false} | ${'asdf'} | ${'BUILD_TIMED_OUT'} | ${'CONTENT_STALE_TIMEOUT'} + ${false} | ${'asdf'} | ${'UP_TO_DATE'} | ${'CONTENT_FRESH'} + ${false} | ${'asdf'} | ${'ERROR'} | ${'CONTENT_STALE_ERROR'} + `( + 'should, when contentLoading=$contentLoading and content="$content" and activeSyncState=$activeSyncState, resolve to $expected', + ({ contentLoading, content, activeSyncState, expected }) => { + expect( + calculateDisplayState({ + contentLoading, + content, + activeSyncState, + }), + ).toEqual(expected); + }, + ); + }); + + describe('reducer', () => { + const contentReloadFn = jest.fn(); + const oldState: Parameters[0] = { + activeSyncState: 'CHECKING', + contentIsStale: false, + contentLoading: false, + path: '', + contentReload: contentReloadFn, + }; + + it('should return a copy of the state', () => { + expect(reducer(oldState, { type: 'navigate', path: '/' })).toEqual({ + activeSyncState: 'CHECKING', + contentIsStale: false, + contentLoading: false, + path: '/', + contentReload: contentReloadFn, + }); + + expect(oldState).toEqual({ + activeSyncState: 'CHECKING', + contentIsStale: false, + contentLoading: false, + path: '', + contentReload: contentReloadFn, + }); + }); + + describe('"content" action', () => { + it('should work', () => { + expect( + reducer( + { + ...oldState, + content: undefined, + contentLoading: true, + contentReload: undefined, + }, + { + type: 'content', + content: 'asdf', + contentLoading: false, + contentReload: contentReloadFn, + }, + ), + ).toEqual({ + ...oldState, + contentLoading: false, + content: 'asdf', + }); + + expect(contentReloadFn).toBeCalledTimes(0); + }); + + it('should reset staleness', () => { + expect( + reducer( + { + ...oldState, + contentIsStale: true, + activeSyncState: 'BUILD_READY', + }, + { + type: 'content', + content: 'asdf', + contentLoading: false, + contentReload: contentReloadFn, + }, + ), + ).toEqual({ + ...oldState, + content: 'asdf', + contentIsStale: false, + activeSyncState: 'UP_TO_DATE', + }); + }); + }); + + describe('"navigate" action', () => { + it('should work', () => { + expect( + reducer(oldState, { + type: 'navigate', + path: '/', + }), + ).toEqual({ + ...oldState, + path: '/', + }); + + expect(contentReloadFn).toBeCalledTimes(0); + }); + + it('should reset staleness', () => { + expect( + reducer( + { + ...oldState, + contentIsStale: true, + activeSyncState: 'BUILD_READY', + }, + { + type: 'navigate', + path: '', + }, + ), + ).toEqual({ + ...oldState, + contentIsStale: false, + activeSyncState: 'UP_TO_DATE', + }); + }); + }); + + describe('"sync" action', () => { + it('should update state', () => { + expect( + reducer(oldState, { + type: 'sync', + state: 'BUILDING', + }), + ).toEqual({ + ...oldState, + activeSyncState: 'BUILDING', + }); + + expect(contentReloadFn).toBeCalledTimes(0); + }); + + it('should set content to be stale but not reload', () => { + expect( + reducer( + { + ...oldState, + contentReload: undefined, + }, + { + type: 'sync', + state: 'BUILD_READY', + }, + ), + ).toEqual({ + ...oldState, + activeSyncState: 'BUILD_READY', + contentIsStale: true, + contentReload: undefined, + }); + + expect(contentReloadFn).toBeCalledTimes(0); + }); + + it('should not reload existing content', () => { + expect( + reducer( + { + ...oldState, + content: 'any content', + }, + { + type: 'sync', + state: 'BUILD_READY', + }, + ), + ).toEqual({ + ...oldState, + activeSyncState: 'BUILD_READY', + contentIsStale: true, + content: 'any content', + }); + + expect(contentReloadFn).toBeCalledTimes(0); + }); + + it('should trigger a reload', () => { + expect( + reducer(oldState, { + type: 'sync', + state: 'BUILD_READY', + }), + ).toEqual({ + ...oldState, + activeSyncState: 'BUILD_READY', + contentIsStale: true, + contentLoading: true, + }); + + expect(contentReloadFn).toBeCalledTimes(1); + }); + + it('should NOT reset staleness', () => { + expect( + reducer( + { + ...oldState, + contentIsStale: true, + activeSyncState: 'BUILD_READY', + }, + { + type: 'sync', + state: 'BUILDING', + }, + ), + ).toEqual({ + ...oldState, + contentIsStale: true, + activeSyncState: 'BUILDING', + }); + }); + }); + }); + + describe('hook', () => { + it('should handle up-to-date content', async () => { + techdocsStorageApi.getEntityDocs.mockResolvedValue('my content'); + techdocsStorageApi.syncEntityDocs.mockImplementation(async () => { + return 'cached'; + }); + + await act(async () => { + const { result, waitForValueToChange } = await renderHook( + () => useReaderState('Component', 'default', 'backstage', '/example'), + { wrapper: Wrapper }, + ); + + expect(result.current).toEqual({ + state: 'CHECKING', + content: undefined, + errorMessage: '', + }); + + await waitForValueToChange(() => result.current.state); + + expect(result.current).toEqual({ + state: 'CONTENT_FRESH', + content: 'my content', + errorMessage: '', + }); + + expect(techdocsStorageApi.getEntityDocs).toBeCalledWith( + { kind: 'Component', namespace: 'default', name: 'backstage' }, + '/example', + ); + expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith({ + kind: 'Component', + namespace: 'default', + name: 'backstage', + }); + }); + }); + + it('should handle stale content', async () => { + techdocsStorageApi.getEntityDocs.mockResolvedValue('my content'); + techdocsStorageApi.syncEntityDocs.mockImplementation(async () => { + await new Promise(resolve => setTimeout(resolve, 1100)); + return 'updated'; + }); + + await act(async () => { + const { result, waitForValueToChange } = await renderHook( + () => useReaderState('Component', 'default', 'backstage', '/example'), + { wrapper: Wrapper }, + ); + + expect(result.current).toEqual({ + state: 'CHECKING', + content: undefined, + errorMessage: '', + }); + + // the content is returned but the sync is in progress + await waitForValueToChange(() => result.current.state); + expect(result.current).toEqual({ + state: 'CONTENT_FRESH', + content: 'my content', + errorMessage: '', + }); + + // the sync takes longer than 1 seconds so the refreshing state starts + await waitForValueToChange(() => result.current.state); + expect(result.current).toEqual({ + state: 'CONTENT_STALE_REFRESHING', + content: 'my content', + errorMessage: '', + }); + + // the content is up-to-date + await waitForValueToChange(() => result.current.state); + expect(result.current).toEqual({ + state: 'CONTENT_STALE_READY', + content: 'my content', + errorMessage: '', + }); + + expect(techdocsStorageApi.getEntityDocs).toBeCalledWith( + { kind: 'Component', namespace: 'default', name: 'backstage' }, + '/example', + ); + expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith({ + kind: 'Component', + namespace: 'default', + name: 'backstage', + }); + }); + }); + + it('should handle timed-out refresh', async () => { + techdocsStorageApi.getEntityDocs.mockResolvedValue('my content'); + techdocsStorageApi.syncEntityDocs.mockResolvedValue('timeout'); + + await act(async () => { + const { result, waitForValueToChange } = await renderHook( + () => useReaderState('Component', 'default', 'backstage', '/example'), + { wrapper: Wrapper }, + ); + + expect(result.current).toEqual({ + state: 'CHECKING', + content: undefined, + errorMessage: '', + }); + + // the content is returned but the sync is in progress + await waitForValueToChange(() => result.current.state); + expect(result.current).toEqual({ + state: 'CONTENT_STALE_TIMEOUT', + content: 'my content', + errorMessage: '', + }); + + expect(techdocsStorageApi.getEntityDocs).toBeCalledWith( + { kind: 'Component', namespace: 'default', name: 'backstage' }, + '/example', + ); + expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith({ + kind: 'Component', + namespace: 'default', + name: 'backstage', + }); + }); + }); + + it('should handle content error', async () => { + techdocsStorageApi.getEntityDocs.mockRejectedValue( + new NotFoundError('Some error description'), + ); + techdocsStorageApi.syncEntityDocs.mockResolvedValue('cached'); + + await act(async () => { + const { result, waitForValueToChange } = await renderHook( + () => useReaderState('Component', 'default', 'backstage', '/example'), + { wrapper: Wrapper }, + ); + + expect(result.current).toEqual({ + state: 'CHECKING', + content: undefined, + errorMessage: '', + }); + + // the content loading threw an error + await waitForValueToChange(() => result.current.state); + expect(result.current).toEqual({ + state: 'CONTENT_NOT_FOUND', + content: undefined, + errorMessage: ' Load error: NotFoundError: Some error description', + }); + + expect(techdocsStorageApi.getEntityDocs).toBeCalledWith( + { kind: 'Component', namespace: 'default', name: 'backstage' }, + '/example', + ); + expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith({ + kind: 'Component', + namespace: 'default', + name: 'backstage', + }); + }); + }); + }); +}); diff --git a/plugins/techdocs/src/reader/components/useReaderState.ts b/plugins/techdocs/src/reader/components/useReaderState.ts new file mode 100644 index 0000000000..f55e7c26e9 --- /dev/null +++ b/plugins/techdocs/src/reader/components/useReaderState.ts @@ -0,0 +1,335 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useApi } from '@backstage/core'; +import { useEffect, useMemo, useReducer } from 'react'; +import { useAsync, useAsyncRetry } from 'react-use'; +import { techdocsStorageApiRef } from '../../api'; + +/** + * A state representation that is used to configure the UI of + */ +type ContentStateTypes = + /** There is nothing to display but a loading indicator */ + | 'CHECKING' + + /** There is no content yet -> present a full screen loading page */ + | 'INITIAL_BUILD' + + /** There is content, but the backend is about to update it */ + | 'CONTENT_STALE_REFRESHING' + + /** There is content, but after a reload, the content will be different */ + | 'CONTENT_STALE_READY' + + /** There is content, the backend tried to update it, but it took too long */ + | 'CONTENT_STALE_TIMEOUT' + + /** There is content, the backend tried to update it, but failed */ + | 'CONTENT_STALE_ERROR' + + /** There is nothing to see but a "not found" page. Is also shown on page load errors */ + | 'CONTENT_NOT_FOUND' + + /** There is only the latest and greatest content */ + | 'CONTENT_FRESH'; + +/** + * Calculate the state that should be reported to the display component. + */ +export function calculateDisplayState({ + contentLoading, + content, + activeSyncState, +}: Pick< + ReducerState, + 'contentLoading' | 'content' | 'activeSyncState' +>): ContentStateTypes { + // we have nothing to display yet + if (contentLoading) { + return 'CHECKING'; + } + + // there is no content, but the sync process is still evaluating + if (!content && activeSyncState === 'CHECKING') { + return 'CHECKING'; + } + + // there is no content yet so we assume that we are building it for the first time + if (!content && activeSyncState === 'BUILDING') { + return 'INITIAL_BUILD'; + } + + // if there is still no content after building, it might just not exist + if (!content) { + return 'CONTENT_NOT_FOUND'; + } + + // we are still building, but we already show stale content + if (activeSyncState === 'BUILDING') { + return 'CONTENT_STALE_REFRESHING'; + } + + // the build is ready, but the content is still stale + if (activeSyncState === 'BUILD_READY') { + return 'CONTENT_STALE_READY'; + } + + // the build timed out, but the content is still stale + if (activeSyncState === 'BUILD_TIMED_OUT') { + return 'CONTENT_STALE_TIMEOUT'; + } + + // the build failed, but the content is still stale + if (activeSyncState === 'ERROR') { + return 'CONTENT_STALE_ERROR'; + } + + // seems like the content is up-to-date (or we don't know yet and the sync process is still evaluating in the background) + return 'CONTENT_FRESH'; +} + +/** + * The state of the synchronization task. It checks whether the docs are + * up-to-date. If they aren't, it triggers a build. + */ +type SyncStates = + /** Checking if it should be synced */ + | 'CHECKING' + + /** Building the documentation */ + | 'BUILDING' + + /** Finished building the documentation */ + | 'BUILD_READY' + + /** Building the documentation timed out */ + | 'BUILD_TIMED_OUT' + + /** No need for a sync. The content was already up-to-date. */ + | 'UP_TO_DATE' + + /** An error occurred */ + | 'ERROR'; + +type ReducerActions = + | { + type: 'sync'; + state: SyncStates; + syncError?: Error; + } + | { + type: 'content'; + content?: string; + contentLoading: boolean; + contentError?: Error; + contentReload: () => void; + } + | { type: 'navigate'; path: string }; + +type ReducerState = { + /** + * The path of the current page + */ + path: string; + + /** + * The current sync state + */ + activeSyncState: SyncStates; + + /** + * If true, the content is downloading from the storage. + */ + contentLoading: boolean; + /** + * The content that has been downloaded and should be displayed. + */ + content?: string; + /** + * When called, the content is reloaded without refreshing the page. + */ + contentReload?: () => void; + /** + * If true, the content is considered stale and should be refreshed by the user via a refresh or a navigation. + */ + contentIsStale: boolean; + + contentError?: Error; + syncError?: Error; +}; + +export function reducer( + oldState: ReducerState, + action: ReducerActions, +): ReducerState { + const newState = { ...oldState }; + + switch (action.type) { + case 'sync': + newState.activeSyncState = action.state; + newState.syncError = action.syncError; + + // whatever is stored as content, it can be considered as being stale + if (newState.activeSyncState === 'BUILD_READY') { + newState.contentIsStale = true; + + // reload the content if this was the initial build OR the page was missing in the old version + if (!newState.content && newState.contentReload) { + newState.contentReload(); + + // eagerly mark the content to load to not get synchronization issues since + // the async hook behind contentReload() doesn't update the reducer instantly + // and might flash the "not found" page + newState.contentLoading = true; + } + } + break; + + case 'content': + newState.content = action.content; + newState.contentLoading = action.contentLoading; + newState.contentReload = action.contentReload; + newState.contentError = action.contentError; + break; + + case 'navigate': + newState.path = action.path; + break; + + default: + throw new Error(); + } + + // a navigation or a content update removes the staleness and resets the sync state + if ( + newState.contentIsStale && + ['content', 'navigate'].includes(action.type) + ) { + newState.contentIsStale = false; + newState.activeSyncState = 'UP_TO_DATE'; + } + + return newState; +} + +export function useReaderState( + kind: string, + namespace: string, + name: string, + path: string, +): { state: ContentStateTypes; content?: string; errorMessage?: string } { + const [state, dispatch] = useReducer(reducer, { + activeSyncState: 'CHECKING', + path, + contentLoading: true, + contentIsStale: false, + }); + + const techdocsStorageApi = useApi(techdocsStorageApiRef); + + // convert all path changes into actions + useEffect(() => { + dispatch({ type: 'navigate', path }); + }, [path]); + + // try to load the content + const { + value: content, + loading: contentLoading, + error: contentError, + retry: contentReload, + } = useAsyncRetry( + async () => + techdocsStorageApi.getEntityDocs( + { + kind, + namespace, + name, + }, + path, + ), + [techdocsStorageApi, kind, namespace, name, path], + ); + + // convert all content changes into actions + useEffect(() => { + dispatch({ + type: 'content', + content, + contentLoading, + contentReload, + contentError, + }); + }, [dispatch, content, contentLoading, contentReload, contentError]); + + // try to derive the state. the function will fire events and we don't care for the return values + useAsync(async () => { + dispatch({ type: 'sync', state: 'CHECKING' }); + + // should only switch to BUILDING if the request takes more than 1 seconds + const buildingTimeout = setTimeout(() => { + dispatch({ type: 'sync', state: 'BUILDING' }); + }, 1000); + + try { + const result = await techdocsStorageApi.syncEntityDocs({ + kind, + namespace, + name, + }); + + if (result === 'updated') { + dispatch({ type: 'sync', state: 'BUILD_READY' }); + } else if (result === 'cached') { + dispatch({ type: 'sync', state: 'UP_TO_DATE' }); + } else { + dispatch({ type: 'sync', state: 'BUILD_TIMED_OUT' }); + } + } catch (e) { + dispatch({ type: 'sync', state: 'ERROR', syncError: e }); + } finally { + // Cancel the timer that sets the state "BUILDING" + clearTimeout(buildingTimeout); + } + }, [kind, name, namespace, techdocsStorageApi, dispatch]); + + const displayState = useMemo( + () => + calculateDisplayState({ + activeSyncState: state.activeSyncState, + contentLoading: state.contentLoading, + content: state.content, + }), + [state.activeSyncState, state.content, state.contentLoading], + ); + + const errorMessage = useMemo(() => { + let errMessage = ''; + if (state.contentError) { + errMessage += ` Load error: ${state.contentError}`; + } + if (state.syncError) errMessage += ` Build error: ${state.syncError}`; + + return errMessage; + }, [state.syncError, state.contentError]); + + return { + state: displayState, + content, + errorMessage, + }; +} diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index 6b6eae4b0d..9bfcbe624a 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -27,7 +27,7 @@ const techdocsStorageApi: TechDocsStorageApi = { Promise.resolve(new URL(o, DOC_STORAGE_URL).toString()), ), getEntityDocs: () => new Promise(resolve => resolve('yes!')), - syncEntityDocs: () => new Promise(resolve => resolve(true)), + syncEntityDocs: () => new Promise(resolve => resolve('updated')), getApiOrigin: jest.fn(() => new Promise(resolve => resolve(API_ORIGIN_URL))), getBuilder: jest.fn(), getStorageUrl: jest.fn(), From 2b9d30b1535f22a1d65fb11021cefb1c1baed916 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Wed, 16 Jun 2021 11:04:26 +0200 Subject: [PATCH 175/206] Fix minor issues from the review comments Signed-off-by: Dominik Henneke --- plugins/techdocs-backend/src/service/router.ts | 9 +++++---- plugins/techdocs/src/client.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 1a8bdb9c8f..ff47e16bc9 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -176,6 +176,11 @@ export async function createRouter({ // This block should be valid for all storage implementations. So no need to duplicate in future, // add the publisher type in the list here. const updated = await docsBuilder.build(); + + if (!updated) { + throw new NotModifiedError(); + } + // With a maximum of ~5 seconds wait, check if the files got published and if docs will be fetched // on the user's page. If not, respond with a message asking them to check back later. // The delay here is to make sure GCS/AWS/etc. registers newly uploaded files which is usually <1 second @@ -195,10 +200,6 @@ export async function createRouter({ ); } - if (!updated) { - throw new NotModifiedError(); - } - res .status(201) .json({ message: 'Docs updated or did not need updating' }); diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 16617dcbce..83cfc88d56 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -192,7 +192,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { * Check if docs are on the latest version and trigger rebuild if not * * @param {EntityName} entityId Object containing entity data like name, namespace, etc. - * @returns {boolean} Whether documents are currently synchronized to newest version + * @returns {SyncResult} Whether documents are currently synchronized to newest version * @throws {Error} Throws error on error from sync endpoint in Techdocs Backend */ async syncEntityDocs(entityId: EntityName): Promise { From ba984f675a121df2260e3a7c717d59a8448cf219 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Wed, 16 Jun 2021 11:04:47 +0200 Subject: [PATCH 176/206] Add a BUILD_READY_RELOAD type that replaces the old contentIsStale logic Signed-off-by: Dominik Henneke --- .../reader/components/useReaderState.test.tsx | 271 +++++++++--------- .../src/reader/components/useReaderState.ts | 114 ++++---- 2 files changed, 180 insertions(+), 205 deletions(-) diff --git a/plugins/techdocs/src/reader/components/useReaderState.test.tsx b/plugins/techdocs/src/reader/components/useReaderState.test.tsx index d5579ddd5a..8a09241588 100644 --- a/plugins/techdocs/src/reader/components/useReaderState.test.tsx +++ b/plugins/techdocs/src/reader/components/useReaderState.test.tsx @@ -49,20 +49,22 @@ describe('useReaderState', () => { describe('calculateDisplayState', () => { it.each` - contentLoading | content | activeSyncState | expected - ${true} | ${''} | ${''} | ${'CHECKING'} - ${false} | ${undefined} | ${'CHECKING'} | ${'CHECKING'} - ${false} | ${undefined} | ${'BUILDING'} | ${'INITIAL_BUILD'} - ${false} | ${undefined} | ${'BUILD_READY'} | ${'CONTENT_NOT_FOUND'} - ${false} | ${undefined} | ${'BUILD_TIMED_OUT'} | ${'CONTENT_NOT_FOUND'} - ${false} | ${undefined} | ${'UP_TO_DATE'} | ${'CONTENT_NOT_FOUND'} - ${false} | ${undefined} | ${'ERROR'} | ${'CONTENT_NOT_FOUND'} - ${false} | ${'asdf'} | ${'CHECKING'} | ${'CONTENT_FRESH'} - ${false} | ${'asdf'} | ${'BUILDING'} | ${'CONTENT_STALE_REFRESHING'} - ${false} | ${'asdf'} | ${'BUILD_READY'} | ${'CONTENT_STALE_READY'} - ${false} | ${'asdf'} | ${'BUILD_TIMED_OUT'} | ${'CONTENT_STALE_TIMEOUT'} - ${false} | ${'asdf'} | ${'UP_TO_DATE'} | ${'CONTENT_FRESH'} - ${false} | ${'asdf'} | ${'ERROR'} | ${'CONTENT_STALE_ERROR'} + contentLoading | content | activeSyncState | expected + ${true} | ${''} | ${''} | ${'CHECKING'} + ${false} | ${undefined} | ${'CHECKING'} | ${'CHECKING'} + ${false} | ${undefined} | ${'BUILDING'} | ${'INITIAL_BUILD'} + ${false} | ${undefined} | ${'BUILD_READY'} | ${'CONTENT_NOT_FOUND'} + ${false} | ${undefined} | ${'BUILD_READY_RELOAD'} | ${'CHECKING'} + ${false} | ${undefined} | ${'BUILD_TIMED_OUT'} | ${'CONTENT_NOT_FOUND'} + ${false} | ${undefined} | ${'UP_TO_DATE'} | ${'CONTENT_NOT_FOUND'} + ${false} | ${undefined} | ${'ERROR'} | ${'CONTENT_NOT_FOUND'} + ${false} | ${'asdf'} | ${'CHECKING'} | ${'CONTENT_FRESH'} + ${false} | ${'asdf'} | ${'BUILDING'} | ${'CONTENT_STALE_REFRESHING'} + ${false} | ${'asdf'} | ${'BUILD_READY'} | ${'CONTENT_STALE_READY'} + ${false} | ${'asdf'} | ${'BUILD_READY_RELOAD'} | ${'CHECKING'} + ${false} | ${'asdf'} | ${'BUILD_TIMED_OUT'} | ${'CONTENT_STALE_TIMEOUT'} + ${false} | ${'asdf'} | ${'UP_TO_DATE'} | ${'CONTENT_FRESH'} + ${false} | ${'asdf'} | ${'ERROR'} | ${'CONTENT_STALE_ERROR'} `( 'should, when contentLoading=$contentLoading and content="$content" and activeSyncState=$activeSyncState, resolve to $expected', ({ contentLoading, content, activeSyncState, expected }) => { @@ -78,48 +80,80 @@ describe('useReaderState', () => { }); describe('reducer', () => { - const contentReloadFn = jest.fn(); const oldState: Parameters[0] = { activeSyncState: 'CHECKING', - contentIsStale: false, contentLoading: false, path: '', - contentReload: contentReloadFn, }; it('should return a copy of the state', () => { expect(reducer(oldState, { type: 'navigate', path: '/' })).toEqual({ activeSyncState: 'CHECKING', - contentIsStale: false, contentLoading: false, path: '/', - contentReload: contentReloadFn, }); expect(oldState).toEqual({ activeSyncState: 'CHECKING', - contentIsStale: false, contentLoading: false, path: '', - contentReload: contentReloadFn, }); }); - describe('"content" action', () => { - it('should work', () => { + it.each` + type | oldActiveSyncState | newActiveSyncState + ${'content'} | ${'BUILD_READY'} | ${'UP_TO_DATE'} + ${'content'} | ${'BUILD_READY_RELOAD'} | ${'UP_TO_DATE'} + ${'navigate'} | ${'BUILD_READY'} | ${'UP_TO_DATE'} + ${'navigate'} | ${'BUILD_READY_RELOAD'} | ${'UP_TO_DATE'} + ${'sync'} | ${'BUILD_READY'} | ${undefined} + ${'sync'} | ${'BUILD_READY_RELOAD'} | ${undefined} + `( + 'should, when type=$type and activeSyncState=$oldActiveSyncState, set activeSyncState=$newActiveSyncState', + ({ type, oldActiveSyncState, newActiveSyncState }) => { expect( reducer( { ...oldState, - content: undefined, + activeSyncState: oldActiveSyncState, + }, + { type }, + ).activeSyncState, + ).toEqual(newActiveSyncState); + }, + ); + + describe('"content" action', () => { + it('should set loading', () => { + expect( + reducer( + { + ...oldState, + content: 'some-old-content', + contentError: new Error(), + }, + { + type: 'content', contentLoading: true, - contentReload: undefined, + }, + ), + ).toEqual({ + ...oldState, + contentLoading: true, + }); + }); + + it('should set content', () => { + expect( + reducer( + { + ...oldState, + contentLoading: true, + contentError: new Error(), }, { type: 'content', content: 'asdf', - contentLoading: false, - contentReload: contentReloadFn, }, ), ).toEqual({ @@ -127,30 +161,25 @@ describe('useReaderState', () => { contentLoading: false, content: 'asdf', }); - - expect(contentReloadFn).toBeCalledTimes(0); }); - it('should reset staleness', () => { + it('should set error', () => { expect( reducer( { ...oldState, - contentIsStale: true, - activeSyncState: 'BUILD_READY', + contentLoading: true, + content: 'asdf', }, { type: 'content', - content: 'asdf', - contentLoading: false, - contentReload: contentReloadFn, + contentError: new Error(), }, ), ).toEqual({ ...oldState, - content: 'asdf', - contentIsStale: false, - activeSyncState: 'UP_TO_DATE', + contentLoading: false, + contentError: new Error(), }); }); }); @@ -166,28 +195,6 @@ describe('useReaderState', () => { ...oldState, path: '/', }); - - expect(contentReloadFn).toBeCalledTimes(0); - }); - - it('should reset staleness', () => { - expect( - reducer( - { - ...oldState, - contentIsStale: true, - activeSyncState: 'BUILD_READY', - }, - { - type: 'navigate', - path: '', - }, - ), - ).toEqual({ - ...oldState, - contentIsStale: false, - activeSyncState: 'UP_TO_DATE', - }); }); }); @@ -202,88 +209,6 @@ describe('useReaderState', () => { ...oldState, activeSyncState: 'BUILDING', }); - - expect(contentReloadFn).toBeCalledTimes(0); - }); - - it('should set content to be stale but not reload', () => { - expect( - reducer( - { - ...oldState, - contentReload: undefined, - }, - { - type: 'sync', - state: 'BUILD_READY', - }, - ), - ).toEqual({ - ...oldState, - activeSyncState: 'BUILD_READY', - contentIsStale: true, - contentReload: undefined, - }); - - expect(contentReloadFn).toBeCalledTimes(0); - }); - - it('should not reload existing content', () => { - expect( - reducer( - { - ...oldState, - content: 'any content', - }, - { - type: 'sync', - state: 'BUILD_READY', - }, - ), - ).toEqual({ - ...oldState, - activeSyncState: 'BUILD_READY', - contentIsStale: true, - content: 'any content', - }); - - expect(contentReloadFn).toBeCalledTimes(0); - }); - - it('should trigger a reload', () => { - expect( - reducer(oldState, { - type: 'sync', - state: 'BUILD_READY', - }), - ).toEqual({ - ...oldState, - activeSyncState: 'BUILD_READY', - contentIsStale: true, - contentLoading: true, - }); - - expect(contentReloadFn).toBeCalledTimes(1); - }); - - it('should NOT reset staleness', () => { - expect( - reducer( - { - ...oldState, - contentIsStale: true, - activeSyncState: 'BUILD_READY', - }, - { - type: 'sync', - state: 'BUILDING', - }, - ), - ).toEqual({ - ...oldState, - contentIsStale: true, - activeSyncState: 'BUILDING', - }); }); }); }); @@ -327,6 +252,68 @@ describe('useReaderState', () => { }); }); + it('should reload initially missing content', async () => { + techdocsStorageApi.getEntityDocs + .mockRejectedValueOnce(new NotFoundError('Page Not Found')) + .mockImplementationOnce(async () => { + await new Promise(resolve => setTimeout(resolve, 500)); + return 'my content'; + }); + techdocsStorageApi.syncEntityDocs.mockImplementation(async () => { + await new Promise(resolve => setTimeout(resolve, 1100)); + return 'updated'; + }); + + await act(async () => { + const { result, waitForValueToChange } = await renderHook( + () => useReaderState('Component', 'default', 'backstage', '/example'), + { wrapper: Wrapper }, + ); + + expect(result.current).toEqual({ + state: 'CHECKING', + content: undefined, + errorMessage: '', + }); + + await waitForValueToChange(() => result.current.state); + + expect(result.current).toEqual({ + state: 'INITIAL_BUILD', + content: undefined, + errorMessage: ' Load error: NotFoundError: Page Not Found', + }); + + await waitForValueToChange(() => result.current.state); + + expect(result.current).toEqual({ + state: 'CHECKING', + content: undefined, + errorMessage: '', + }); + + await waitForValueToChange(() => result.current.state); + + expect(result.current).toEqual({ + state: 'CONTENT_FRESH', + content: 'my content', + errorMessage: '', + }); + + expect(techdocsStorageApi.getEntityDocs).toBeCalledTimes(2); + expect(techdocsStorageApi.getEntityDocs).toBeCalledWith( + { kind: 'Component', namespace: 'default', name: 'backstage' }, + '/example', + ); + expect(techdocsStorageApi.syncEntityDocs).toBeCalledTimes(1); + expect(techdocsStorageApi.syncEntityDocs).toBeCalledWith({ + kind: 'Component', + namespace: 'default', + name: 'backstage', + }); + }); + }); + it('should handle stale content', async () => { techdocsStorageApi.getEntityDocs.mockResolvedValue('my content'); techdocsStorageApi.syncEntityDocs.mockImplementation(async () => { diff --git a/plugins/techdocs/src/reader/components/useReaderState.ts b/plugins/techdocs/src/reader/components/useReaderState.ts index f55e7c26e9..1dc4bc2677 100644 --- a/plugins/techdocs/src/reader/components/useReaderState.ts +++ b/plugins/techdocs/src/reader/components/useReaderState.ts @@ -15,7 +15,7 @@ */ import { useApi } from '@backstage/core'; -import { useEffect, useMemo, useReducer } from 'react'; +import { useEffect, useMemo, useReducer, useRef } from 'react'; import { useAsync, useAsyncRetry } from 'react-use'; import { techdocsStorageApiRef } from '../../api'; @@ -63,6 +63,11 @@ export function calculateDisplayState({ return 'CHECKING'; } + // the build is ready, but it triggered a content reload and the content variable is not trusted + if (activeSyncState === 'BUILD_READY_RELOAD') { + return 'CHECKING'; + } + // there is no content, but the sync process is still evaluating if (!content && activeSyncState === 'CHECKING') { return 'CHECKING'; @@ -116,6 +121,12 @@ type SyncStates = /** Finished building the documentation */ | 'BUILD_READY' + /** + * Finished building the documentation and triggered a content reload. + * This state is left toward UP_TO_DATE when the content loading has finished. + */ + | 'BUILD_READY_RELOAD' + /** Building the documentation timed out */ | 'BUILD_TIMED_OUT' @@ -134,9 +145,8 @@ type ReducerActions = | { type: 'content'; content?: string; - contentLoading: boolean; + contentLoading?: true; contentError?: Error; - contentReload: () => void; } | { type: 'navigate'; path: string }; @@ -159,14 +169,6 @@ type ReducerState = { * The content that has been downloaded and should be displayed. */ content?: string; - /** - * When called, the content is reloaded without refreshing the page. - */ - contentReload?: () => void; - /** - * If true, the content is considered stale and should be refreshed by the user via a refresh or a navigation. - */ - contentIsStale: boolean; contentError?: Error; syncError?: Error; @@ -182,27 +184,11 @@ export function reducer( case 'sync': newState.activeSyncState = action.state; newState.syncError = action.syncError; - - // whatever is stored as content, it can be considered as being stale - if (newState.activeSyncState === 'BUILD_READY') { - newState.contentIsStale = true; - - // reload the content if this was the initial build OR the page was missing in the old version - if (!newState.content && newState.contentReload) { - newState.contentReload(); - - // eagerly mark the content to load to not get synchronization issues since - // the async hook behind contentReload() doesn't update the reducer instantly - // and might flash the "not found" page - newState.contentLoading = true; - } - } break; case 'content': newState.content = action.content; - newState.contentLoading = action.contentLoading; - newState.contentReload = action.contentReload; + newState.contentLoading = action.contentLoading ?? false; newState.contentError = action.contentError; break; @@ -214,12 +200,11 @@ export function reducer( throw new Error(); } - // a navigation or a content update removes the staleness and resets the sync state + // a navigation or a content update loads fresh content so the build is updated to being up-to-date if ( - newState.contentIsStale && + ['BUILD_READY', 'BUILD_READY_RELOAD'].includes(newState.activeSyncState) && ['content', 'navigate'].includes(action.type) ) { - newState.contentIsStale = false; newState.activeSyncState = 'UP_TO_DATE'; } @@ -236,7 +221,6 @@ export function useReaderState( activeSyncState: 'CHECKING', path, contentLoading: true, - contentIsStale: false, }); const techdocsStorageApi = useApi(techdocsStorageApiRef); @@ -246,35 +230,33 @@ export function useReaderState( dispatch({ type: 'navigate', path }); }, [path]); - // try to load the content - const { - value: content, - loading: contentLoading, - error: contentError, - retry: contentReload, - } = useAsyncRetry( - async () => - techdocsStorageApi.getEntityDocs( - { - kind, - namespace, - name, - }, - path, - ), - [techdocsStorageApi, kind, namespace, name, path], - ); + // try to load the content. the function will fire events and we don't care for the return values + const { retry: contentReload } = useAsyncRetry(async () => { + dispatch({ type: 'content', contentLoading: true }); - // convert all content changes into actions - useEffect(() => { - dispatch({ - type: 'content', - content, - contentLoading, - contentReload, - contentError, - }); - }, [dispatch, content, contentLoading, contentReload, contentError]); + try { + const entityDocs = await techdocsStorageApi.getEntityDocs( + { kind, namespace, name }, + path, + ); + + dispatch({ type: 'content', content: entityDocs }); + + return entityDocs; + } catch (e) { + dispatch({ type: 'content', contentError: e }); + } + + return undefined; + }, [techdocsStorageApi, kind, namespace, name, path]); + + // create a ref that holds the latest content. This provides a useAsync hook + // with the latest content without restarting the useAsync hook. + const contentRef = useRef<{ content?: string; reload: () => void }>({ + content: undefined, + reload: () => {}, + }); + contentRef.current = { content: state.content, reload: contentReload }; // try to derive the state. the function will fire events and we don't care for the return values useAsync(async () => { @@ -293,7 +275,13 @@ export function useReaderState( }); if (result === 'updated') { - dispatch({ type: 'sync', state: 'BUILD_READY' }); + // if there was no content prior to building, retry the loading + if (!contentRef.current.content) { + contentRef.current.reload(); + dispatch({ type: 'sync', state: 'BUILD_READY_RELOAD' }); + } else { + dispatch({ type: 'sync', state: 'BUILD_READY' }); + } } else if (result === 'cached') { dispatch({ type: 'sync', state: 'UP_TO_DATE' }); } else { @@ -305,7 +293,7 @@ export function useReaderState( // Cancel the timer that sets the state "BUILDING" clearTimeout(buildingTimeout); } - }, [kind, name, namespace, techdocsStorageApi, dispatch]); + }, [kind, name, namespace, techdocsStorageApi, dispatch, contentRef]); const displayState = useMemo( () => @@ -329,7 +317,7 @@ export function useReaderState( return { state: displayState, - content, + content: state.content, errorMessage, }; } From 5429bfa69edd7e28ab00d5832b9f9e433dba44c7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Jun 2021 10:22:43 +0200 Subject: [PATCH 177/206] scripts/api-extractor: update to create reports for plugin packages too Signed-off-by: Patrik Oldsberg --- scripts/api-extractor.ts | 74 ++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index f4aa0a7ddc..9ba59543f8 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -60,34 +60,60 @@ PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = function tryGetPackag return old.call(this, path); }; -const DOCUMENTED_PACKAGES = [ - 'packages/backend-common', - 'packages/backend-test-utils', - 'packages/catalog-client', - 'packages/catalog-model', - 'packages/cli-common', - 'packages/config', - 'packages/config-loader', - 'packages/core-app-api', +const PACKAGE_ROOTS = ['packages', 'plugins']; + +const SKIPPED_PACKAGES = [ + 'packages/app', + 'packages/backend', + 'packages/cli', + 'packages/codemods', + 'packages/create-app', + 'packages/docgen', + 'packages/e2e-test', + 'packages/storybook', + 'packages/techdocs-cli', + // TODO(Rugvip): Enable these once `import * as ...` and `import()` PRs have landed, #1796 & #1916. - // 'packages/core-components', - 'packages/core-plugin-api', - 'packages/dev-utils', - 'packages/errors', - 'packages/integration', - 'packages/integration-react', - 'packages/search-common', - 'packages/techdocs-common', - 'packages/test-utils', - 'packages/test-utils-core', - 'packages/theme', + 'packages/core', + 'packages/core-api', + 'packages/core-components', + 'plugins/catalog', + 'plugins/catalog-backend', + 'plugins/catalog-react', + 'plugins/github-deployments', + 'plugins/sentry-backend', ]; +async function findPackageDirs() { + const packageDirs = new Array(); + const projectRoot = resolvePath(__dirname, '..'); + + for (const packageRoot of PACKAGE_ROOTS) { + const dirs = await fs.readdir(resolvePath(projectRoot, packageRoot)); + for (const dir of dirs) { + const fullPackageDir = resolvePath(packageRoot, dir); + + const stat = await fs.stat(fullPackageDir); + if (!stat.isDirectory()) { + continue; + } + + const packageDir = relativePath(projectRoot, fullPackageDir); + if (!SKIPPED_PACKAGES.includes(packageDir)) { + packageDirs.push(packageDir); + } + } + } + + return packageDirs; +} + interface ApiExtractionOptions { packageDirs: string[]; outputDir: string; isLocalBuild: boolean; } + async function runApiExtraction({ packageDirs, outputDir, @@ -110,7 +136,9 @@ async function runApiExtraction({ configObject: { mainEntryPointFilePath: resolvePath( __dirname, - '../dist-types/packages//src/index.d.ts', + '../dist-types', + packageDir, + 'src/index.d.ts', ), bundledPackages: [], @@ -307,9 +335,11 @@ async function main() { const isCiBuild = process.argv.includes('--ci'); const isDocsBuild = process.argv.includes('--docs'); + const packageDirs = await findPackageDirs(); + console.log('# Generating package API reports'); await runApiExtraction({ - packageDirs: DOCUMENTED_PACKAGES, + packageDirs, outputDir: tmpDir, isLocalBuild: !isCiBuild, }); From cebde9d9ac0a8ded3e61edad7419750615bfdecf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Jun 2021 10:36:50 +0200 Subject: [PATCH 178/206] create-app: fix release version to 0.3.26 Signed-off-by: Patrik Oldsberg --- packages/create-app/CHANGELOG.md | 2 +- packages/create-app/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index ba40e15f26..a1df0f294c 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,6 +1,6 @@ # @backstage/create-app -## 1.0.0 +## 0.3.26 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 1373c52c71..95add27d6a 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "1.0.0", + "version": "0.3.26", "private": false, "publishConfig": { "access": "public" From 74f8b8fcb1d5ce24ba8f30a812ff4178fd0a33ba Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Jun 2021 11:20:27 +0200 Subject: [PATCH 179/206] catalog-model: deprecated usage of yup-based validators Signed-off-by: Patrik Oldsberg --- packages/catalog-model/api-report.md | 6 +++--- packages/catalog-model/src/location/validation.ts | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 0dab9670de..68644bef87 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -10,7 +10,7 @@ import { JsonValue } from '@backstage/config'; import { SerializedError } from '@backstage/errors'; import * as yup from 'yup'; -// @public (undocumented) +// @public @deprecated (undocumented) export const analyzeLocationSchema: yup.ObjectSchema<{ location: LocationSpec; }, object>; @@ -316,7 +316,7 @@ export { LocationEntityV1alpha1 } // @public (undocumented) export const locationEntityV1alpha1Validator: KindValidator; -// @public (undocumented) +// @public @deprecated (undocumented) export const locationSchema: yup.ObjectSchema; // @public (undocumented) @@ -326,7 +326,7 @@ export type LocationSpec = { presence?: 'optional' | 'required'; }; -// @public (undocumented) +// @public @deprecated (undocumented) export const locationSpecSchema: yup.ObjectSchema; // @public (undocumented) diff --git a/packages/catalog-model/src/location/validation.ts b/packages/catalog-model/src/location/validation.ts index 4d2e602862..3a2fee5089 100644 --- a/packages/catalog-model/src/location/validation.ts +++ b/packages/catalog-model/src/location/validation.ts @@ -17,6 +17,7 @@ import * as yup from 'yup'; import { LocationSpec, Location } from './types'; +/** @deprecated */ export const locationSpecSchema = yup .object({ type: yup.string().required(), @@ -26,6 +27,7 @@ export const locationSpecSchema = yup .noUnknown() .required(); +/** @deprecated */ export const locationSchema = yup .object({ id: yup.string().required(), @@ -35,6 +37,7 @@ export const locationSchema = yup .noUnknown() .required(); +/** @deprecated */ export const analyzeLocationSchema = yup .object<{ location: LocationSpec }>({ location: locationSpecSchema, From d4f20e5be3555e28213cdaec89c3093c67734bf7 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 17 Jun 2021 11:40:22 +0200 Subject: [PATCH 180/206] chore: dont replace empty strings to undefined in `input` parsing Signed-off-by: blam --- .../src/scaffolder/tasks/TaskWorker.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 94eaa62c85..bff10fc7e9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -170,11 +170,6 @@ export class TaskWorker { preventIndent: true, })(templateCtx); - // If it's just an empty string, treat it as undefined - if (templated === '') { - return undefined; - } - // If it smells like a JSON object then give it a parse as an object and if it fails return the string if ( (templated.startsWith('"') && templated.endsWith('"')) || @@ -213,6 +208,10 @@ export class TaskWorker { // Keep track of all tmp dirs that are created by the action so we can remove them after const tmpDirs = new Array(); + this.options.logger.debug(`Running ${action.id} with input`, { + input: JSON.stringify(input, null, 2), + }); + await action.handler({ baseUrl: task.spec.baseUrl, logger: taskLogger, From b492221760757d4686cbae533f4b5f1242136f5e Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 17 Jun 2021 11:41:56 +0200 Subject: [PATCH 181/206] chore: added changeset Signed-off-by: blam --- .changeset/unlucky-peas-nail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/unlucky-peas-nail.md diff --git a/.changeset/unlucky-peas-nail.md b/.changeset/unlucky-peas-nail.md new file mode 100644 index 0000000000..a4b5da1f90 --- /dev/null +++ b/.changeset/unlucky-peas-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Keep the empty string as empty string in `input` rather than replacing with `undefined` to make empty values ok for `cookiecutter` From d5db15efbc6abc81e48988323ab5def2fd2d8875 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 17 Jun 2021 12:27:01 +0200 Subject: [PATCH 182/206] Add IdentityApi to plugin-search Signed-off-by: Daniel Johansson --- .changeset/good-jars-turn.md | 5 +++++ plugins/search/package.json | 2 +- plugins/search/src/apis.test.ts | 28 +++++++++++++++++++++++++++- plugins/search/src/apis.ts | 14 +++++++++++--- plugins/search/src/plugin.ts | 7 ++++--- 5 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 .changeset/good-jars-turn.md diff --git a/.changeset/good-jars-turn.md b/.changeset/good-jars-turn.md new file mode 100644 index 0000000000..3ba4cb1d36 --- /dev/null +++ b/.changeset/good-jars-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': minor +--- + +add IdentityApi support diff --git a/plugins/search/package.json b/plugins/search/package.json index 5c84748e06..dc0041c7a7 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.4.0", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts index 64a5207d6d..09b8dc48e6 100644 --- a/plugins/search/src/apis.test.ts +++ b/plugins/search/src/apis.test.ts @@ -26,8 +26,20 @@ describe('apis', () => { const baseUrl = 'https://base-url.com/'; const getBaseUrl = jest.fn().mockResolvedValue(baseUrl); + + const token = 'AUTHTOKEN'; + const withToken = jest.fn().mockResolvedValue(token); + const withoutToken = jest.fn().mockResolvedValue(undefined); + const createIdentityApiMock = (getIdToken: any) => ({ + getIdToken, + getUserId: jest.fn(), + getProfile: jest.fn(), + signOut: jest.fn(), + }); + const client = new SearchClient({ discoveryApi: { getBaseUrl }, + identityApi: createIdentityApiMock(withoutToken), }); const json = jest.fn(); @@ -41,7 +53,21 @@ describe('apis', () => { it('Fetch is called with expected URL (including stringified Q params)', async () => { await client.query(query); expect(getBaseUrl).toHaveBeenLastCalledWith('search/query'); - expect(fetch).toHaveBeenLastCalledWith(`${baseUrl}?term=&pageCursor=`); + expect(fetch).toHaveBeenLastCalledWith(`${baseUrl}?term=&pageCursor=`, { + headers: {}, + }); + }); + + it('Sets Authorization if token is available', async () => { + const authedClient = new SearchClient({ + discoveryApi: { getBaseUrl }, + identityApi: createIdentityApiMock(withToken), + }); + await authedClient.query(query); + expect(getBaseUrl).toHaveBeenLastCalledWith('search/query'); + expect(fetch).toHaveBeenLastCalledWith(`${baseUrl}?term=&pageCursor=`, { + headers: { Authorization: `Bearer ${token}` }, + }); }); it('Resolves JSON from fetch response', async () => { diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index 5e039cbb84..8d618116be 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef, DiscoveryApi } from '@backstage/core'; +import { createApiRef, DiscoveryApi, IdentityApi } from '@backstage/core'; import { SearchQuery, SearchResultSet } from '@backstage/search-common'; import qs from 'qs'; @@ -29,17 +29,25 @@ export interface SearchApi { export class SearchClient implements SearchApi { private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; - constructor(options: { discoveryApi: DiscoveryApi }) { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; } async query(query: SearchQuery): Promise { + const token = await this.identityApi.getIdToken(); const queryString = qs.stringify(query); const url = `${await this.discoveryApi.getBaseUrl( 'search/query', )}?${queryString}`; - const response = await fetch(url); + const response = await fetch(url, { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); return response.json(); } } diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 45987a3749..a0f2103c5f 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -20,6 +20,7 @@ import { createRoutableExtension, discoveryApiRef, createComponentExtension, + identityApiRef, } from '@backstage/core'; import { SearchClient, searchApiRef } from './apis'; @@ -38,9 +39,9 @@ export const searchPlugin = createPlugin({ apis: [ createApiFactory({ api: searchApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => { - return new SearchClient({ discoveryApi }); + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => { + return new SearchClient({ discoveryApi, identityApi }); }, }), ], From f4fbbf1552a4bd95a33261da58cbcc5c7ba4c7f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 17 Jun 2021 11:08:46 +0000 Subject: [PATCH 183/206] Version Packages --- .changeset/unlucky-peas-nail.md | 5 ----- packages/create-app/CHANGELOG.md | 7 +++++++ packages/create-app/package.json | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 6 ++++++ plugins/scaffolder-backend/package.json | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) delete mode 100644 .changeset/unlucky-peas-nail.md diff --git a/.changeset/unlucky-peas-nail.md b/.changeset/unlucky-peas-nail.md deleted file mode 100644 index a4b5da1f90..0000000000 --- a/.changeset/unlucky-peas-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Keep the empty string as empty string in `input` rather than replacing with `undefined` to make empty values ok for `cookiecutter` diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index a1df0f294c..4658e413c1 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/create-app +## 0.3.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.12.2 + ## 0.3.26 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 95add27d6a..0eb5d9e29c 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.26", + "version": "0.3.27", "private": false, "publishConfig": { "access": "public" diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 4ea79dfeed..f747fce610 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-scaffolder-backend +## 0.12.2 + +### Patch Changes + +- b49222176: Keep the empty string as empty string in `input` rather than replacing with `undefined` to make empty values ok for `cookiecutter` + ## 0.12.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index b9737bc2fc..f3e76720af 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.12.1", + "version": "0.12.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From e3d31b3815e406b89ec894eaa2f5d108c4d6cb25 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Jun 2021 12:44:21 +0200 Subject: [PATCH 184/206] cli: disable GitHub App webhook by default Signed-off-by: Patrik Oldsberg --- .changeset/odd-humans-exercise.md | 5 +++++ docs/plugins/github-apps.md | 4 ++++ .../src/commands/create-github-app/GithubCreateAppServer.ts | 1 + 3 files changed, 10 insertions(+) create mode 100644 .changeset/odd-humans-exercise.md diff --git a/.changeset/odd-humans-exercise.md b/.changeset/odd-humans-exercise.md new file mode 100644 index 0000000000..136ec86b69 --- /dev/null +++ b/.changeset/odd-humans-exercise.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Make the `create-github-app` command disable webhooks by default. diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md index 87d23b45d5..51d3fb9854 100644 --- a/docs/plugins/github-apps.md +++ b/docs/plugins/github-apps.md @@ -43,6 +43,10 @@ root of the project which you can then use as an `include` in your `app-config.yaml`. You can go ahead and [skip ahead](#including-in-integrations-config) if you've already got an app. +Note that the created app will have a webhook that is disabled by default and +points to `smee.io`, which is intended for local development. There's also +currently no part of Backstage that makes use of the webhook. + ### GitHub Enterprise You have to create the GitHub Application manually using these diff --git a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts index 45671c2ead..0ffc1a08ff 100644 --- a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts +++ b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts @@ -120,6 +120,7 @@ export class GithubCreateAppServer { redirect_url: `${baseUrl}/callback`, hook_attributes: { url: this.webhookUrl, + active: false, }, }; const manifestJson = JSON.stringify(manifest).replace(/\"/g, '"'); From d8d7226fce8ed099a7dffd2ad1fac0f9d62c0675 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Jun 2021 11:00:16 +0200 Subject: [PATCH 185/206] plugins: generate api reports Signed-off-by: Patrik Oldsberg --- plugins/api-docs/api-report.md | 115 ++++ plugins/app-backend/api-report.md | 28 + plugins/auth-backend/api-report.md | 211 +++++++ plugins/badges-backend/api-report.md | 113 ++++ plugins/badges/api-report.md | 21 + plugins/bitrise/api-report.md | 22 + plugins/catalog-graphql/api-report.md | 25 + plugins/catalog-import/api-report.md | 130 ++++ plugins/circleci/api-report.md | 81 +++ plugins/cloudbuild/api-report.md | 283 +++++++++ plugins/code-coverage-backend/api-report.md | 43 ++ plugins/code-coverage/api-report.md | 32 + plugins/config-schema/api-report.md | 42 ++ plugins/cost-insights/api-report.md | 621 ++++++++++++++++++++ plugins/explore-react/api-report.md | 31 + plugins/explore/api-report.md | 38 ++ plugins/fossa/api-report.md | 27 + plugins/gcp-projects/api-report.md | 86 +++ plugins/git-release-manager/api-report.md | 25 + plugins/github-actions/api-report.md | 213 +++++++ plugins/gitops-profiles/api-report.md | 197 +++++++ plugins/graphiql/api-report.md | 82 +++ plugins/graphql/api-report.md | 25 + plugins/ilert/api-report.md | 205 +++++++ plugins/jenkins/api-report.md | 83 +++ plugins/kafka-backend/api-report.md | 17 + plugins/kafka/api-report.md | 41 ++ plugins/kubernetes-backend/api-report.md | 103 ++++ plugins/kubernetes-common/api-report.md | 130 ++++ plugins/kubernetes/api-report.md | 46 ++ plugins/lighthouse/api-report.md | 186 ++++++ plugins/newrelic/api-report.md | 25 + plugins/org/api-report.md | 69 +++ plugins/pagerduty/api-report.md | 62 ++ plugins/proxy-backend/api-report.md | 18 + plugins/register-component/api-report.md | 32 + plugins/rollbar-backend/api-report.md | 60 ++ plugins/rollbar/api-report.md | 78 +++ plugins/scaffolder-backend/api-report.md | 518 ++++++++++++++++ plugins/scaffolder/api-report.md | 112 ++++ plugins/search-backend-node/api-report.md | 68 +++ plugins/search-backend/api-report.md | 17 + plugins/search/api-report.md | 102 ++++ plugins/sentry/api-report.md | 100 ++++ plugins/shortcuts/api-report.md | 56 ++ plugins/sonarqube/api-report.md | 41 ++ plugins/splunk-on-call/api-report.md | 68 +++ plugins/tech-radar/api-report.md | 123 ++++ plugins/techdocs-backend/api-report.md | 24 + plugins/techdocs/api-report.md | 146 +++++ plugins/todo-backend/api-report.md | 98 +++ plugins/todo/api-report.md | 23 + plugins/user-settings/api-report.md | 45 ++ plugins/welcome/api-report.md | 22 + 54 files changed, 5209 insertions(+) create mode 100644 plugins/api-docs/api-report.md create mode 100644 plugins/app-backend/api-report.md create mode 100644 plugins/auth-backend/api-report.md create mode 100644 plugins/badges-backend/api-report.md create mode 100644 plugins/badges/api-report.md create mode 100644 plugins/bitrise/api-report.md create mode 100644 plugins/catalog-graphql/api-report.md create mode 100644 plugins/catalog-import/api-report.md create mode 100644 plugins/circleci/api-report.md create mode 100644 plugins/cloudbuild/api-report.md create mode 100644 plugins/code-coverage-backend/api-report.md create mode 100644 plugins/code-coverage/api-report.md create mode 100644 plugins/config-schema/api-report.md create mode 100644 plugins/cost-insights/api-report.md create mode 100644 plugins/explore-react/api-report.md create mode 100644 plugins/explore/api-report.md create mode 100644 plugins/fossa/api-report.md create mode 100644 plugins/gcp-projects/api-report.md create mode 100644 plugins/git-release-manager/api-report.md create mode 100644 plugins/github-actions/api-report.md create mode 100644 plugins/gitops-profiles/api-report.md create mode 100644 plugins/graphiql/api-report.md create mode 100644 plugins/graphql/api-report.md create mode 100644 plugins/ilert/api-report.md create mode 100644 plugins/jenkins/api-report.md create mode 100644 plugins/kafka-backend/api-report.md create mode 100644 plugins/kafka/api-report.md create mode 100644 plugins/kubernetes-backend/api-report.md create mode 100644 plugins/kubernetes-common/api-report.md create mode 100644 plugins/kubernetes/api-report.md create mode 100644 plugins/lighthouse/api-report.md create mode 100644 plugins/newrelic/api-report.md create mode 100644 plugins/org/api-report.md create mode 100644 plugins/pagerduty/api-report.md create mode 100644 plugins/proxy-backend/api-report.md create mode 100644 plugins/register-component/api-report.md create mode 100644 plugins/rollbar-backend/api-report.md create mode 100644 plugins/rollbar/api-report.md create mode 100644 plugins/scaffolder-backend/api-report.md create mode 100644 plugins/scaffolder/api-report.md create mode 100644 plugins/search-backend-node/api-report.md create mode 100644 plugins/search-backend/api-report.md create mode 100644 plugins/search/api-report.md create mode 100644 plugins/sentry/api-report.md create mode 100644 plugins/shortcuts/api-report.md create mode 100644 plugins/sonarqube/api-report.md create mode 100644 plugins/splunk-on-call/api-report.md create mode 100644 plugins/tech-radar/api-report.md create mode 100644 plugins/techdocs-backend/api-report.md create mode 100644 plugins/techdocs/api-report.md create mode 100644 plugins/todo-backend/api-report.md create mode 100644 plugins/todo/api-report.md create mode 100644 plugins/user-settings/api-report.md create mode 100644 plugins/welcome/api-report.md diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md new file mode 100644 index 0000000000..5a09cfceea --- /dev/null +++ b/plugins/api-docs/api-report.md @@ -0,0 +1,115 @@ +## API Report File for "@backstage/plugin-api-docs" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiEntity } from '@backstage/catalog-model'; +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { CatalogTableRow } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; +import { ExternalRouteRef } from '@backstage/core'; +import { default as React_2 } from 'react'; +import { RouteRef } from '@backstage/core'; +import { TableColumn } from '@backstage/core'; +import { UserListFilterKind } from '@backstage/plugin-catalog-react'; + +// @public (undocumented) +export const ApiDefinitionCard: (_: Props) => JSX.Element; + +// @public (undocumented) +export type ApiDefinitionWidget = { + type: string; + title: string; + component: (definition: string) => React_2.ReactElement; + rawLanguage?: string; +}; + +// @public (undocumented) +export const apiDocsConfigRef: ApiRef; + +// @public (undocumented) +const apiDocsPlugin: BackstagePlugin<{ + root: RouteRef; +}, { + createComponent: ExternalRouteRef; +}>; + +export { apiDocsPlugin } + +export { apiDocsPlugin as plugin } + +// @public (undocumented) +export const ApiExplorerPage: ({ initiallySelectedFilter, columns, }: ApiExplorerPageProps) => JSX.Element; + +// @public (undocumented) +export const ApiTypeTitle: ({ apiEntity }: { + apiEntity: ApiEntity; +}) => JSX.Element; + +// @public (undocumented) +export const AsyncApiDefinitionWidget: ({ definition }: Props_5) => JSX.Element; + +// @public (undocumented) +export const ConsumedApisCard: ({ variant }: Props_2) => JSX.Element; + +// @public (undocumented) +export const ConsumingComponentsCard: ({ variant }: Props_6) => JSX.Element; + +// @public (undocumented) +export function defaultDefinitionWidgets(): ApiDefinitionWidget[]; + +// @public (undocumented) +export const EntityApiDefinitionCard: (_: { + apiEntity?: ApiEntity | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityConsumedApisCard: ({ variant }: { + entity?: Entity| undefined; + variant?: "gridItem" | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityConsumingComponentsCard: ({ variant }: { + entity?: Entity| undefined; + variant?: "gridItem" | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityHasApisCard: ({ variant }: { + variant?: "gridItem" | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityProvidedApisCard: ({ variant }: { + entity?: Entity| undefined; + variant?: "gridItem" | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityProvidingComponentsCard: ({ variant }: { + entity?: Entity| undefined; + variant?: "gridItem" | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const HasApisCard: ({ variant }: Props_3) => JSX.Element; + +// @public (undocumented) +export const OpenApiDefinitionWidget: ({ definition }: Props_8) => JSX.Element; + +// @public (undocumented) +export const PlainApiDefinitionWidget: ({ definition, language }: Props_9) => JSX.Element; + +// @public (undocumented) +export const ProvidedApisCard: ({ variant }: Props_4) => JSX.Element; + +// @public (undocumented) +export const ProvidingComponentsCard: ({ variant }: Props_7) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/app-backend/api-report.md b/plugins/app-backend/api-report.md new file mode 100644 index 0000000000..4f59102265 --- /dev/null +++ b/plugins/app-backend/api-report.md @@ -0,0 +1,28 @@ +## API Report File for "@backstage/plugin-app-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Config } from '@backstage/config'; +import express from 'express'; +import { Logger } from 'winston'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export interface RouterOptions { + appPackageName: string; + // (undocumented) + config: Config; + disableConfigInjection?: boolean; + // (undocumented) + logger: Logger; + staticFallbackHandler?: express.Handler; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md new file mode 100644 index 0000000000..dea5500d53 --- /dev/null +++ b/plugins/auth-backend/api-report.md @@ -0,0 +1,211 @@ +## API Report File for "@backstage/plugin-auth-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CatalogApi } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import express from 'express'; +import { JSONWebKey } from 'jose'; +import { Logger } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Profile } from 'passport'; + +// @public (undocumented) +export type AuthProviderFactory = (options: AuthProviderFactoryOptions) => AuthProviderRouteHandlers; + +// @public (undocumented) +export type AuthProviderFactoryOptions = { + providerId: string; + globalConfig: AuthProviderConfig; + config: Config; + logger: Logger; + tokenIssuer: TokenIssuer; + discovery: PluginEndpointDiscovery; + catalogApi: CatalogApi; + identityResolver?: ExperimentalIdentityResolver; +}; + +// @public +export interface AuthProviderRouteHandlers { + frameHandler(req: express.Request, res: express.Response): Promise; + logout?(req: express.Request, res: express.Response): Promise; + refresh?(req: express.Request, res: express.Response): Promise; + start(req: express.Request, res: express.Response): Promise; +} + +// @public (undocumented) +export type AuthResponse = { + providerInfo: ProviderInfo; + profile: ProfileInfo; + backstageIdentity?: BackstageIdentity; +}; + +// @public (undocumented) +export type BackstageIdentity = { + id: string; + idToken?: string; +}; + +// @public (undocumented) +export function createRouter({ logger, config, discovery, database, providerFactories, }: RouterOptions): Promise; + +// @public (undocumented) +export const defaultAuthProviderFactories: { + [providerId: string]: AuthProviderFactory; +}; + +// @public (undocumented) +export const encodeState: (state: OAuthState) => string; + +// @public (undocumented) +export const ensuresXRequestedWith: (req: express.Request) => boolean; + +// @public +export class IdentityClient { + constructor(options: { + discovery: PluginEndpointDiscovery; + issuer: string; + }); + authenticate(token: string | undefined): Promise; + static getBearerToken(authorizationHeader: string | undefined): string | undefined; + listPublicKeys(): Promise<{ + keys: JSONWebKey[]; + }>; + } + +// @public (undocumented) +export class OAuthAdapter implements AuthProviderRouteHandlers { + constructor(handlers: OAuthHandlers, options: Options); + // (undocumented) + frameHandler(req: express.Request, res: express.Response): Promise; + // (undocumented) + static fromConfig(config: AuthProviderConfig, handlers: OAuthHandlers, options: Pick): OAuthAdapter; + // (undocumented) + logout(req: express.Request, res: express.Response): Promise; + // (undocumented) + refresh(req: express.Request, res: express.Response): Promise; + // (undocumented) + start(req: express.Request, res: express.Response): Promise; +} + +// @public (undocumented) +export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { + constructor(handlers: Map); + // (undocumented) + frameHandler(req: express.Request, res: express.Response): Promise; + // (undocumented) + logout(req: express.Request, res: express.Response): Promise; + // (undocumented) + static mapConfig(config: Config, factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers): OAuthEnvironmentHandler; + // (undocumented) + refresh(req: express.Request, res: express.Response): Promise; + // (undocumented) + start(req: express.Request, res: express.Response): Promise; +} + +// @public +export interface OAuthHandlers { + handler(req: express.Request): Promise<{ + response: AuthResponse; + refreshToken?: string; + }>; + logout?(): Promise; + refresh?(req: OAuthRefreshRequest): Promise>; + start(req: OAuthStartRequest): Promise; +} + +// @public (undocumented) +export type OAuthProviderInfo = { + accessToken: string; + idToken?: string; + expiresInSeconds?: number; + scope: string; + refreshToken?: string; +}; + +// @public +export type OAuthProviderOptions = { + clientId: string; + clientSecret: string; + callbackUrl: string; +}; + +// @public (undocumented) +export type OAuthRefreshRequest = express.Request<{}> & { + scope: string; + refreshToken: string; +}; + +// @public (undocumented) +export type OAuthResponse = AuthResponse; + +// @public (undocumented) +export type OAuthResult = { + fullProfile: Profile; + params: { + id_token?: string; + scope: string; + expires_in: number; + }; + accessToken: string; + refreshToken?: string; +}; + +// @public (undocumented) +export type OAuthStartRequest = express.Request<{}> & { + scope: string; + state: OAuthState; +}; + +// @public (undocumented) +export type OAuthState = { + nonce: string; + env: string; +}; + +// @public (undocumented) +export const postMessageResponse: (res: express.Response, appOrigin: string, response: WebMessageResponse) => void; + +// @public +export type ProfileInfo = { + email?: string; + displayName?: string; + picture?: string; +}; + +// @public (undocumented) +export const readState: (stateString: string) => OAuthState; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + config: Config; + // (undocumented) + database: PluginDatabaseManager; + // (undocumented) + discovery: PluginEndpointDiscovery; + // (undocumented) + logger: Logger; + // (undocumented) + providerFactories?: ProviderFactories; +} + +// @public (undocumented) +export const verifyNonce: (req: express.Request, providerId: string) => void; + +// @public +export type WebMessageResponse = { + type: 'authorization_response'; + response: AuthResponse; +} | { + type: 'authorization_response'; + error: Error; +}; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md new file mode 100644 index 0000000000..1523d685b4 --- /dev/null +++ b/plugins/badges-backend/api-report.md @@ -0,0 +1,113 @@ +## API Report File for "@backstage/plugin-badges-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CatalogApi } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; +import express from 'express'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public (undocumented) +export interface Badge { + color?: string; + description?: string; + kind?: 'entity'; + label: string; + labelColor?: string; + link?: string; + message: string; + style?: BadgeStyle; +} + +// @public (undocumented) +export const BADGE_STYLES: readonly ["plastic", "flat", "flat-square", "for-the-badge", "social"]; + +// @public (undocumented) +export type BadgeBuilder = { + getBadges(): Promise; + createBadgeJson(options: BadgeOptions): Promise; + createBadgeSvg(options: BadgeOptions): Promise; +}; + +// @public (undocumented) +export interface BadgeContext { + // (undocumented) + badgeUrl: string; + // (undocumented) + config: Config; + // (undocumented) + entity?: Entity; +} + +// @public (undocumented) +export interface BadgeFactories { + // (undocumented) + [id: string]: BadgeFactory; +} + +// @public (undocumented) +export interface BadgeFactory { + // (undocumented) + createBadge(context: BadgeContext): Badge; +} + +// @public (undocumented) +export type BadgeInfo = { + id: string; +}; + +// @public (undocumented) +export type BadgeOptions = { + badgeInfo: BadgeInfo; + context: BadgeContext; +}; + +// @public (undocumented) +export type BadgeSpec = { + id: string; + badge: Badge; + url: string; + markdown: string; +}; + +// @public (undocumented) +export type BadgeStyle = typeof BADGE_STYLES[number]; + +// @public (undocumented) +export const createDefaultBadgeFactories: () => BadgeFactories; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export class DefaultBadgeBuilder implements BadgeBuilder { + constructor(factories: BadgeFactories); + // (undocumented) + createBadgeJson(options: BadgeOptions): Promise; + // (undocumented) + createBadgeSvg(options: BadgeOptions): Promise; + // (undocumented) + getBadges(): Promise; + } + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + badgeBuilder?: BadgeBuilder; + // (undocumented) + badgeFactories?: BadgeFactories; + // (undocumented) + catalog?: CatalogApi; + // (undocumented) + config: Config; + // (undocumented) + discovery: PluginEndpointDiscovery; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/badges/api-report.md b/plugins/badges/api-report.md new file mode 100644 index 0000000000..dfe7cbdae8 --- /dev/null +++ b/plugins/badges/api-report.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/plugin-badges" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; + +// @public (undocumented) +export const badgesPlugin: BackstagePlugin<{}, {}>; + +// @public (undocumented) +export const EntityBadgesDialog: ({ open, onClose }: { + open: boolean; + onClose?: (() => any) | undefined; +}) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/bitrise/api-report.md b/plugins/bitrise/api-report.md new file mode 100644 index 0000000000..c07178111d --- /dev/null +++ b/plugins/bitrise/api-report.md @@ -0,0 +1,22 @@ +## API Report File for "@backstage/plugin-bitrise" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; + +// @public (undocumented) +export const bitrisePlugin: BackstagePlugin<{}, {}>; + +// @public (undocumented) +export const EntityBitriseContent: () => JSX.Element; + +// @public (undocumented) +export const isBitriseAvailable: (entity: Entity) => boolean; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/catalog-graphql/api-report.md b/plugins/catalog-graphql/api-report.md new file mode 100644 index 0000000000..cbe325f4c5 --- /dev/null +++ b/plugins/catalog-graphql/api-report.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-catalog-graphql" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Config } from '@backstage/config'; +import { GraphQLModule } from '@graphql-modules/core'; +import { Logger } from 'winston'; + +// @public (undocumented) +export function createModule(options: ModuleOptions): Promise; + +// @public (undocumented) +export interface ModuleOptions { + // (undocumented) + config: Config; + // (undocumented) + logger: Logger; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md new file mode 100644 index 0000000000..34baed46b5 --- /dev/null +++ b/plugins/catalog-import/api-report.md @@ -0,0 +1,130 @@ +## API Report File for "@backstage/plugin-catalog-import" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { CatalogApi } from '@backstage/catalog-client'; +import { ConfigApi } from '@backstage/core'; +import { Control } from 'react-hook-form'; +import { DiscoveryApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { EntityName } from '@backstage/catalog-model'; +import { FieldErrors } from 'react-hook-form'; +import { IdentityApi } from '@backstage/core'; +import { InfoCardVariants } from '@backstage/core'; +import { OAuthApi } from '@backstage/core'; +import { default as React_2 } from 'react'; +import { RouteRef } from '@backstage/core'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { SubmitHandler } from 'react-hook-form'; +import { TextFieldProps } from '@material-ui/core/TextField/TextField'; +import { UnpackNestedValue } from 'react-hook-form'; +import { UseControllerOptions } from 'react-hook-form'; +import { UseFormMethods } from 'react-hook-form'; +import { UseFormOptions } from 'react-hook-form'; + +// @public (undocumented) +export type AnalyzeResult = { + type: 'locations'; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; +} | { + type: 'repository'; + url: string; + integrationType: string; + generatedEntities: PartialEntity[]; +}; + +// @public (undocumented) +export const AutocompleteTextField: ({ name, options, required, control, errors, rules, loading, loadingText, helperText, errorHelperText, textFieldProps, }: Props_4) => JSX.Element; + +// @public (undocumented) +export interface CatalogImportApi { + // (undocumented) + analyzeUrl(url: string): Promise; + // (undocumented) + submitPullRequest(options: { + repositoryUrl: string; + fileContent: string; + title: string; + body: string; + }): Promise<{ + link: string; + location: string; + }>; +} + +// @public (undocumented) +export const catalogImportApiRef: ApiRef; + +// @public (undocumented) +export class CatalogImportClient implements CatalogImportApi { + constructor(options: { + discoveryApi: DiscoveryApi; + githubAuthApi: OAuthApi; + identityApi: IdentityApi; + scmIntegrationsApi: ScmIntegrationRegistry; + catalogApi: CatalogApi; + }); + // (undocumented) + analyzeUrl(url: string): Promise; + // (undocumented) + submitPullRequest({ repositoryUrl, fileContent, title, body, }: { + repositoryUrl: string; + fileContent: string; + title: string; + body: string; + }): Promise<{ + link: string; + location: string; + }>; +} + +// @public (undocumented) +export const CatalogImportPage: (opts: StepperProviderOpts) => JSX.Element; + +// @public (undocumented) +const catalogImportPlugin: BackstagePlugin<{ + importPage: RouteRef; +}, {}>; + +export { catalogImportPlugin } + +export { catalogImportPlugin as plugin } + +// @public +export function defaultGenerateStepper(flow: ImportFlows, defaults: StepperProvider): StepperProvider; + +// @public (undocumented) +export const EntityListComponent: ({ locations, collapsed, locationListItemIcon, onItemClick, firstListItem, withLinks, }: Props_2) => JSX.Element; + +// @public (undocumented) +export const ImportStepper: ({ initialUrl, generateStepper, variant, opts, }: Props) => JSX.Element; + +// @public +export const PreparePullRequestForm: >({ defaultValues, onSubmit, render, }: Props_5) => JSX.Element; + +// @public (undocumented) +export const PreviewCatalogInfoComponent: ({ repositoryUrl, entities, classes, }: Props_6) => JSX.Element; + +// @public (undocumented) +export const PreviewPullRequestComponent: ({ title, description, classes, }: Props_7) => JSX.Element; + +// @public (undocumented) +export const Router: (opts: StepperProviderOpts) => JSX.Element; + +// @public +export const StepInitAnalyzeUrl: ({ onAnalysis, analysisUrl, disablePullRequest, }: Props_3) => JSX.Element; + +// @public (undocumented) +export const StepPrepareCreatePullRequest: ({ analyzeResult, onPrepare, onGoBack, renderFormFields, defaultTitle, defaultBody, }: Props_8) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/circleci/api-report.md b/plugins/circleci/api-report.md new file mode 100644 index 0000000000..6c774cc6b0 --- /dev/null +++ b/plugins/circleci/api-report.md @@ -0,0 +1,81 @@ +## API Report File for "@backstage/plugin-circleci" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { BuildStepAction } from 'circleci-api'; +import { BuildSummary } from 'circleci-api'; +import { BuildSummaryResponse } from 'circleci-api'; +import { BuildWithSteps } from 'circleci-api'; +import { CircleCIOptions } from 'circleci-api'; +import { DiscoveryApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { GitType } from 'circleci-api'; +import { Me } from 'circleci-api'; +import { RouteRef } from '@backstage/core'; + +export { BuildStepAction } + +export { BuildSummary } + +export { BuildWithSteps } + +// @public (undocumented) +export const CIRCLECI_ANNOTATION = "circleci.com/project-slug"; + +// @public (undocumented) +export class CircleCIApi { + constructor(options: Options); + // (undocumented) + getBuild(buildNumber: number, options: Partial): Promise; + // (undocumented) + getBuilds({ limit, offset }: { + limit: number; + offset: number; + }, options: Partial): Promise; + // (undocumented) + getUser(options: Partial): Promise; + // (undocumented) + retry(buildNumber: number, options: Partial): Promise; +} + +// @public (undocumented) +export const circleCIApiRef: ApiRef; + +// @public (undocumented) +export const circleCIBuildRouteRef: RouteRef; + +// @public (undocumented) +const circleCIPlugin: BackstagePlugin<{}, {}>; + +export { circleCIPlugin } + +export { circleCIPlugin as plugin } + +// @public (undocumented) +export const circleCIRouteRef: RouteRef; + +// @public (undocumented) +export const EntityCircleCIContent: (_props: { + entity?: Entity| undefined; +}) => JSX.Element; + +export { GitType } + +// @public (undocumented) +const isCircleCIAvailable: (entity: Entity) => boolean; + +export { isCircleCIAvailable } + +export { isCircleCIAvailable as isPluginApplicableToEntity } + +// @public (undocumented) +export const Router: (_props: Props) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/cloudbuild/api-report.md b/plugins/cloudbuild/api-report.md new file mode 100644 index 0000000000..58dc59c175 --- /dev/null +++ b/plugins/cloudbuild/api-report.md @@ -0,0 +1,283 @@ +## API Report File for "@backstage/plugin-cloudbuild" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { OAuthApi } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export type ActionsGetWorkflowResponseData = { + id: string; + status: string; + source: Source; + createTime: string; + startTime: string; + steps: Step[]; + timeout: string; + projectId: string; + logsBucket: string; + sourceProvenance: SourceProvenance; + buildTriggerId: string; + options: Options; + logUrl: string; + substitutions: Substitutions; + tags: string[]; + queueTtl: string; + name: string; + finishTime: any; + results: Results; + timing: Timing2; +}; + +// @public (undocumented) +export interface ActionsListWorkflowRunsForRepoResponseData { + // (undocumented) + builds: ActionsGetWorkflowResponseData[]; +} + +// @public (undocumented) +export interface BUILD { + // (undocumented) + endTime: string; + // (undocumented) + startTime: string; +} + +// @public (undocumented) +export const CLOUDBUILD_ANNOTATION = "google.com/cloudbuild-project-slug"; + +// @public (undocumented) +export type CloudbuildApi = { + listWorkflowRuns: (request: { + projectId: string; + }) => Promise; + getWorkflow: ({ projectId, id, }: { + projectId: string; + id: string; + }) => Promise; + getWorkflowRun: ({ projectId, id, }: { + projectId: string; + id: string; + }) => Promise; + reRunWorkflow: ({ projectId, runId, }: { + projectId: string; + runId: string; + }) => Promise; +}; + +// @public (undocumented) +export const cloudbuildApiRef: ApiRef; + +// @public (undocumented) +export class CloudbuildClient implements CloudbuildApi { + constructor(googleAuthApi: OAuthApi); + // (undocumented) + getToken(): Promise; + // (undocumented) + getWorkflow({ projectId, id, }: { + projectId: string; + id: string; + }): Promise; + // (undocumented) + getWorkflowRun({ projectId, id, }: { + projectId: string; + id: string; + }): Promise; + // (undocumented) + listWorkflowRuns({ projectId, }: { + projectId: string; + }): Promise; + // (undocumented) + reRunWorkflow({ projectId, runId, }: { + projectId: string; + runId: string; + }): Promise; +} + +// @public (undocumented) +const cloudbuildPlugin: BackstagePlugin<{ + entityContent: RouteRef; +}, {}>; + +export { cloudbuildPlugin } + +export { cloudbuildPlugin as plugin } + +// @public (undocumented) +export const EntityCloudbuildContent: (_props: { + entity?: Entity| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityLatestCloudbuildRunCard: ({ branch, }: { + entity?: Entity| undefined; + branch: string; +}) => JSX.Element; + +// @public (undocumented) +export const EntityLatestCloudbuildsForBranchCard: ({ branch, }: { + entity?: Entity| undefined; + branch: string; +}) => JSX.Element; + +// @public (undocumented) +export interface FETCHSOURCE { + // (undocumented) + endTime: string; + // (undocumented) + startTime: string; +} + +// @public (undocumented) +const isCloudbuildAvailable: (entity: Entity) => boolean; + +export { isCloudbuildAvailable } + +export { isCloudbuildAvailable as isPluginApplicableToEntity } + +// @public (undocumented) +export const LatestWorkflowRunCard: ({ branch, }: { + entity?: Entity | undefined; + branch: string; +}) => JSX.Element; + +// @public (undocumented) +export const LatestWorkflowsForBranchCard: ({ branch, }: { + entity?: Entity | undefined; + branch: string; +}) => JSX.Element; + +// @public (undocumented) +export interface Options { + // (undocumented) + dynamicSubstitutions: boolean; + // (undocumented) + logging: string; + // (undocumented) + machineType: string; + // (undocumented) + substitutionOption: string; +} + +// @public (undocumented) +export interface PullTiming { + // (undocumented) + endTime: string; + // (undocumented) + startTime: string; +} + +// @public (undocumented) +export interface ResolvedStorageSource { + // (undocumented) + bucket: string; + // (undocumented) + generation: string; + // (undocumented) + object: string; +} + +// @public (undocumented) +export interface Results { + // (undocumented) + buildStepImages: string[]; + // (undocumented) + buildStepOutputs: string[]; +} + +// @public (undocumented) +export const Router: (_props: Props) => JSX.Element; + +// @public (undocumented) +export interface Source { + // (undocumented) + storageSource: StorageSource; +} + +// @public (undocumented) +export interface SourceProvenance { + // (undocumented) + fileHashes: {}; + // (undocumented) + resolvedStorageSource: {}; +} + +// @public (undocumented) +export interface Step { + // (undocumented) + args: string[]; + // (undocumented) + dir: string; + // (undocumented) + entrypoint: string; + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + pullTiming: PullTiming; + // (undocumented) + status: string; + // (undocumented) + timing: Timing; + // (undocumented) + volumes: Volume[]; + // (undocumented) + waitFor: string[]; +} + +// @public (undocumented) +export interface StorageSource { + // (undocumented) + bucket: string; + // (undocumented) + object: string; +} + +// @public (undocumented) +export interface Substitutions { + // (undocumented) + BRANCH_NAME: string; + // (undocumented) + COMMIT_SHA: string; + // (undocumented) + REPO_NAME: string; + // (undocumented) + REVISION_ID: string; + // (undocumented) + SHORT_SHA: string; +} + +// @public (undocumented) +export interface Timing { + // (undocumented) + endTime: string; + // (undocumented) + startTime: string; +} + +// @public (undocumented) +export interface Timing2 { + // (undocumented) + BUILD: BUILD; + // (undocumented) + FETCHSOURCE: FETCHSOURCE; +} + +// @public (undocumented) +export interface Volume { + // (undocumented) + name: string; + // (undocumented) + path: string; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md new file mode 100644 index 0000000000..f8f1575d0b --- /dev/null +++ b/plugins/code-coverage-backend/api-report.md @@ -0,0 +1,43 @@ +## API Report File for "@backstage/plugin-code-coverage-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Config } from '@backstage/config'; +import express from 'express'; +import { Logger } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; + +// @public (undocumented) +export interface CodeCoverageApi { + // (undocumented) + name: string; +} + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export const makeRouter: (options: RouterOptions) => Promise; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + config: Config; + // (undocumented) + database: PluginDatabaseManager; + // (undocumented) + discovery: PluginEndpointDiscovery; + // (undocumented) + logger: Logger; + // (undocumented) + urlReader: UrlReader; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/code-coverage/api-report.md b/plugins/code-coverage/api-report.md new file mode 100644 index 0000000000..bade5ddb8a --- /dev/null +++ b/plugins/code-coverage/api-report.md @@ -0,0 +1,32 @@ +## API Report File for "@backstage/plugin-code-coverage" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const codeCoveragePlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + +// @public (undocumented) +export const EntityCodeCoverageContent: () => JSX.Element; + +// @public (undocumented) +const isCodeCoverageAvailable: (entity: Entity) => boolean; + +export { isCodeCoverageAvailable } + +export { isCodeCoverageAvailable as isPluginApplicableToEntity } + +// @public (undocumented) +export const Router: () => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/config-schema/api-report.md b/plugins/config-schema/api-report.md new file mode 100644 index 0000000000..67497be6b6 --- /dev/null +++ b/plugins/config-schema/api-report.md @@ -0,0 +1,42 @@ +## API Report File for "@backstage/plugin-config-schema" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { Observable } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; +import { Schema } from 'jsonschema'; + +// @public (undocumented) +export interface ConfigSchemaApi { + // (undocumented) + schema$(): Observable; +} + +// @public (undocumented) +export const configSchemaApiRef: ApiRef; + +// @public (undocumented) +export const ConfigSchemaPage: () => JSX.Element; + +// @public (undocumented) +export const configSchemaPlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + +// @public +export class StaticSchemaLoader implements ConfigSchemaApi { + constructor({ url }?: { + url?: string; + }); + // (undocumented) + schema$(): Observable; + } + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/cost-insights/api-report.md b/plugins/cost-insights/api-report.md new file mode 100644 index 0000000000..7476f75cad --- /dev/null +++ b/plugins/cost-insights/api-report.md @@ -0,0 +1,621 @@ +## API Report File for "@backstage/plugin-cost-insights" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePalette } from '@backstage/theme'; +import { BackstagePlugin } from '@backstage/core'; +import { BackstageTheme } from '@backstage/theme'; +import { ContentRenderer } from 'recharts'; +import { Dispatch } from 'react'; +import { ForwardRefExoticComponent } from 'react'; +import { PaletteOptions } from '@material-ui/core/styles/createPalette'; +import { PropsWithChildren } from 'react'; +import { ReactNode } from 'react'; +import { RechartsFunction } from 'recharts'; +import { RefAttributes } from 'react'; +import { RouteRef } from '@backstage/core'; +import { SetStateAction } from 'react'; +import { TooltipProps } from 'recharts'; +import { TypographyProps } from '@material-ui/core'; + +// @public +export type Alert = { + title: string | JSX.Element; + subtitle: string | JSX.Element; + element?: JSX.Element; + status?: AlertStatus; + url?: string; + buttonText?: string; + SnoozeForm?: Maybe; + AcceptForm?: Maybe; + DismissForm?: Maybe; + onSnoozed?(options: AlertOptions): Promise; + onAccepted?(options: AlertOptions): Promise; + onDismissed?(options: AlertOptions): Promise; +}; + +// @public (undocumented) +export interface AlertCost { + // (undocumented) + aggregation: [number, number]; + // (undocumented) + id: string; +} + +// @public (undocumented) +export interface AlertDismissFormData { + // (undocumented) + feedback: Maybe; + // (undocumented) + other: Maybe; + // (undocumented) + reason: AlertDismissReason; +} + +// @public (undocumented) +export interface AlertDismissOption { + // (undocumented) + label: string; + // (undocumented) + reason: string; +} + +// @public (undocumented) +export const AlertDismissOptions: AlertDismissOption[]; + +// @public (undocumented) +export enum AlertDismissReason { + // (undocumented) + Expected = "expected", + // (undocumented) + Migration = "migration", + // (undocumented) + NotApplicable = "not-applicable", + // (undocumented) + Other = "other", + // (undocumented) + Resolved = "resolved", + // (undocumented) + Seasonal = "seasonal" +} + +// @public (undocumented) +export type AlertForm = ForwardRefExoticComponent & RefAttributes>; + +// @public (undocumented) +export type AlertFormProps = { + alert: A; + onSubmit: (data: FormData) => void; + disableSubmit: (isDisabled: boolean) => void; +}; + +// @public (undocumented) +export interface AlertOptions { + // (undocumented) + data: T; + // (undocumented) + group: string; +} + +// @public +export interface AlertSnoozeFormData { + // (undocumented) + intervals: string; +} + +// @public (undocumented) +export type AlertSnoozeOption = { + label: string; + duration: Duration; +}; + +// @public (undocumented) +export const AlertSnoozeOptions: AlertSnoozeOption[]; + +// @public (undocumented) +export enum AlertStatus { + // (undocumented) + Accepted = "accepted", + // (undocumented) + Dismissed = "dismissed", + // (undocumented) + Snoozed = "snoozed" +} + +// @public (undocumented) +export const BarChart: ({ resources, responsive, displayAmount, options, tooltip, onClick, onMouseMove, }: BarChartProps) => JSX.Element; + +// @public +export interface BarChartData extends BarChartOptions { +} + +// @public (undocumented) +export const BarChartLegend: ({ costStart, costEnd, options, children, }: PropsWithChildren) => JSX.Element; + +// @public (undocumented) +export type BarChartLegendOptions = { + previousName: string; + previousFill: string; + currentName: string; + currentFill: string; + hideMarker?: boolean; +}; + +// @public (undocumented) +export type BarChartLegendProps = { + costStart: number; + costEnd: number; + options?: Partial; +}; + +// @public (undocumented) +export interface BarChartOptions { + // (undocumented) + currentFill: string; + // (undocumented) + currentName: string; + // (undocumented) + previousFill: string; + // (undocumented) + previousName: string; +} + +// @public (undocumented) +export type BarChartProps = { + resources: ResourceData[]; + responsive?: boolean; + displayAmount?: number; + options?: Partial; + tooltip?: ContentRenderer; + onClick?: RechartsFunction; + onMouseMove?: RechartsFunction; +}; + +// @public (undocumented) +export const BarChartTooltip: ({ title, content, subtitle, topRight, actions, children, }: PropsWithChildren) => JSX.Element; + +// @public (undocumented) +export const BarChartTooltipItem: ({ item }: BarChartTooltipItemProps) => JSX.Element; + +// @public (undocumented) +export type BarChartTooltipItemProps = { + item: TooltipItem; +}; + +// @public (undocumented) +export type BarChartTooltipProps = { + title: string; + content?: ReactNode | string; + subtitle?: ReactNode; + topRight?: ReactNode; + actions?: ReactNode; +}; + +// @public (undocumented) +export interface ChangeStatistic { + // (undocumented) + amount: number; + // (undocumented) + ratio?: number; +} + +// @public (undocumented) +export enum ChangeThreshold { + // (undocumented) + lower = -0.05, + // (undocumented) + upper = 0.05 +} + +// @public (undocumented) +export type ChartData = { + date: number; + trend: number; + dailyCost: number; + [key: string]: number; +}; + +// @public (undocumented) +export interface Cost { + // (undocumented) + aggregation: DateAggregation[]; + // (undocumented) + change?: ChangeStatistic; + // (undocumented) + groupedCosts?: Record; + // (undocumented) + id: string; + // (undocumented) + trendline?: Trendline; +} + +// @public (undocumented) +export const CostGrowth: ({ change, duration }: CostGrowthProps) => JSX.Element; + +// @public (undocumented) +export const CostGrowthIndicator: ({ change, formatter, className, ...props }: CostGrowthIndicatorProps) => JSX.Element; + +// @public (undocumented) +export type CostGrowthIndicatorProps = TypographyProps & { + change: ChangeStatistic; + formatter?: (change: ChangeStatistic) => Maybe; +}; + +// @public (undocumented) +export type CostGrowthProps = { + change: ChangeStatistic; + duration: Duration; +}; + +// @public (undocumented) +export type CostInsightsApi = { + getLastCompleteBillingDate(): Promise; + getUserGroups(userId: string): Promise; + getGroupProjects(group: string): Promise; + getGroupDailyCost(group: string, intervals: string): Promise; + getProjectDailyCost(project: string, intervals: string): Promise; + getDailyMetricData(metric: string, intervals: string): Promise; + getProductInsights(options: ProductInsightsOptions): Promise; + getAlerts(group: string): Promise; +}; + +// @public (undocumented) +export const costInsightsApiRef: ApiRef; + +// @public (undocumented) +export const CostInsightsLabelDataflowInstructionsPage: () => JSX.Element; + +// @public (undocumented) +export const CostInsightsPage: () => JSX.Element; + +// @public (undocumented) +export type CostInsightsPalette = BackstagePalette & CostInsightsPaletteAdditions; + +// @public (undocumented) +export type CostInsightsPaletteOptions = PaletteOptions & CostInsightsPaletteAdditions; + +// @public (undocumented) +const costInsightsPlugin: BackstagePlugin<{ + root: RouteRef; + growthAlerts: RouteRef; + unlabeledDataflowAlerts: RouteRef; +}, {}>; + +export { costInsightsPlugin } + +export { costInsightsPlugin as plugin } + +// @public (undocumented) +export const CostInsightsProjectGrowthInstructionsPage: () => JSX.Element; + +// @public (undocumented) +export interface CostInsightsTheme extends BackstageTheme { + // (undocumented) + palette: CostInsightsPalette; +} + +// @public (undocumented) +export interface CostInsightsThemeOptions extends PaletteOptions { + // (undocumented) + palette: CostInsightsPaletteOptions; +} + +// @public (undocumented) +export interface Currency { + // (undocumented) + kind: string | null; + // (undocumented) + label: string; + // (undocumented) + prefix?: string; + // (undocumented) + rate?: number; + // (undocumented) + unit: string; +} + +// @public (undocumented) +export enum CurrencyType { + // (undocumented) + Beers = "BEERS", + // (undocumented) + CarbonOffsetTons = "CARBON_OFFSET_TONS", + // (undocumented) + IceCream = "PINTS_OF_ICE_CREAM", + // (undocumented) + USD = "USD" +} + +// @public (undocumented) +export enum DataKey { + // (undocumented) + Current = "current", + // (undocumented) + Name = "name", + // (undocumented) + Previous = "previous" +} + +// @public (undocumented) +export type DateAggregation = { + date: string; + amount: number; +}; + +// @public (undocumented) +export const DEFAULT_DATE_FORMAT = "YYYY-MM-DD"; + +// @public +export enum Duration { + // (undocumented) + P30D = "P30D", + // (undocumented) + P3M = "P3M", + // (undocumented) + P7D = "P7D", + // (undocumented) + P90D = "P90D" +} + +// @public (undocumented) +export const EngineerThreshold = 0.5; + +// @public (undocumented) +export interface Entity { + // (undocumented) + aggregation: [number, number]; + // (undocumented) + change: ChangeStatistic; + // (undocumented) + entities: Record; + // (undocumented) + id: Maybe; +} + +// @public (undocumented) +export class ExampleCostInsightsClient implements CostInsightsApi { + // (undocumented) + getAlerts(group: string): Promise; + // (undocumented) + getDailyMetricData(metric: string, intervals: string): Promise; + // (undocumented) + getGroupDailyCost(group: string, intervals: string): Promise; + // (undocumented) + getGroupProjects(group: string): Promise; + // (undocumented) + getLastCompleteBillingDate(): Promise; + // (undocumented) + getProductInsights(options: ProductInsightsOptions): Promise; + // (undocumented) + getProjectDailyCost(project: string, intervals: string): Promise; + // (undocumented) + getUserGroups(userId: string): Promise; + } + +// @public (undocumented) +export type Group = { + id: string; +}; + +// @public (undocumented) +export enum GrowthType { + // (undocumented) + Excess = 2, + // (undocumented) + Negligible = 0, + // (undocumented) + Savings = 1 +} + +// @public (undocumented) +export type Icon = { + kind: string; + component: JSX.Element; +}; + +// @public (undocumented) +export enum IconType { + // (undocumented) + Compute = "compute", + // (undocumented) + Data = "data", + // (undocumented) + Database = "database", + // (undocumented) + ML = "ml", + // (undocumented) + Search = "search", + // (undocumented) + Storage = "storage" +} + +// @public (undocumented) +export const LegendItem: ({ title, tooltipText, markerColor, children, }: PropsWithChildren) => JSX.Element; + +// @public (undocumented) +export type LegendItemProps = { + title: string; + tooltipText?: string; + markerColor?: string; +}; + +// @public (undocumented) +export type Loading = Record; + +// @public (undocumented) +export type Maybe = T | null; + +// @public (undocumented) +export type Metric = { + kind: string; + name: string; + default: boolean; +}; + +// @public (undocumented) +export interface MetricData { + // (undocumented) + aggregation: DateAggregation[]; + // (undocumented) + change: ChangeStatistic; + // (undocumented) + format: 'number' | 'currency'; + // (undocumented) + id: string; +} + +// @public (undocumented) +export const MockConfigProvider: ({ children, ...context }: MockConfigProviderProps) => JSX.Element; + +// @public (undocumented) +export const MockCurrencyProvider: ({ children, ...context }: MockCurrencyProviderProps) => JSX.Element; + +// @public (undocumented) +export interface PageFilters { + // (undocumented) + duration: Duration; + // (undocumented) + group: Maybe; + // (undocumented) + metric: string | null; + // (undocumented) + project: Maybe; +} + +// @public (undocumented) +export interface Product { + // (undocumented) + kind: string; + // (undocumented) + name: string; +} + +// @public (undocumented) +export type ProductFilters = Array; + +// @public (undocumented) +export type ProductInsightsOptions = { + product: string; + group: string; + intervals: string; + project: Maybe; +}; + +// @public (undocumented) +export interface ProductPeriod { + // (undocumented) + duration: Duration; + // (undocumented) + productType: string; +} + +// @public (undocumented) +export interface Project { + // (undocumented) + id: string; + // (undocumented) + name?: string; +} + +// @public +export class ProjectGrowthAlert implements Alert { + constructor(data: ProjectGrowthData); + // (undocumented) + data: ProjectGrowthData; + // (undocumented) + get element(): JSX.Element; + // (undocumented) + get subtitle(): string; + // (undocumented) + get title(): string; + // (undocumented) + get url(): string; +} + +// @public (undocumented) +export interface ProjectGrowthData { + // (undocumented) + aggregation: [number, number]; + // (undocumented) + change: ChangeStatistic; + // (undocumented) + periodEnd: string; + // (undocumented) + periodStart: string; + // (undocumented) + products: Array; + // (undocumented) + project: string; +} + +// @public (undocumented) +export interface ResourceData { + // (undocumented) + current: number; + // (undocumented) + name: Maybe; + // (undocumented) + previous: number; +} + +// @public (undocumented) +export type TooltipItem = { + fill: string; + label: string; + value: string; +}; + +// @public (undocumented) +export type Trendline = { + slope: number; + intercept: number; +}; + +// @public +export class UnlabeledDataflowAlert implements Alert { + constructor(data: UnlabeledDataflowData); + // (undocumented) + data: UnlabeledDataflowData; + // (undocumented) + get element(): JSX.Element; + // (undocumented) + status?: AlertStatus; + // (undocumented) + get subtitle(): string; + // (undocumented) + get title(): string; + // (undocumented) + get url(): string; +} + +// @public (undocumented) +export interface UnlabeledDataflowAlertProject { + // (undocumented) + id: string; + // (undocumented) + labeledCost: number; + // (undocumented) + unlabeledCost: number; +} + +// @public (undocumented) +export interface UnlabeledDataflowData { + // (undocumented) + labeledCost: number; + // (undocumented) + periodEnd: string; + // (undocumented) + periodStart: string; + // (undocumented) + projects: Array; + // (undocumented) + unlabeledCost: number; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/explore-react/api-report.md b/plugins/explore-react/api-report.md new file mode 100644 index 0000000000..aaf272a06a --- /dev/null +++ b/plugins/explore-react/api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/plugin-explore-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; + +// @public (undocumented) +export type ExploreTool = { + title: string; + description?: string; + url: string; + image: string; + tags?: string[]; + lifecycle?: string; +}; + +// @public (undocumented) +export interface ExploreToolsConfig { + // (undocumented) + getTools: () => Promise; +} + +// @public (undocumented) +export const exploreToolsConfigRef: ApiRef; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md new file mode 100644 index 0000000000..590af53de8 --- /dev/null +++ b/plugins/explore/api-report.md @@ -0,0 +1,38 @@ +## API Report File for "@backstage/plugin-explore" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; +import { ExternalRouteRef } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const catalogEntityRouteRef: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; +}, false>; + +// @public (undocumented) +export const ExplorePage: () => JSX.Element; + +// @public (undocumented) +export const explorePlugin: BackstagePlugin<{ + explore: RouteRef; +}, { + catalogEntity: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }, false>; +}>; + +// @public (undocumented) +export const exploreRouteRef: RouteRef; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/fossa/api-report.md b/plugins/fossa/api-report.md new file mode 100644 index 0000000000..edb2cfdd0a --- /dev/null +++ b/plugins/fossa/api-report.md @@ -0,0 +1,27 @@ +## API Report File for "@backstage/plugin-fossa" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; +import { InfoCardVariants } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const EntityFossaCard: ({ variant }: { + variant?: InfoCardVariants| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const FossaPage: () => JSX.Element; + +// @public (undocumented) +export const fossaPlugin: BackstagePlugin<{ + fossaOverview: RouteRef; +}, {}>; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/gcp-projects/api-report.md b/plugins/gcp-projects/api-report.md new file mode 100644 index 0000000000..22812822c0 --- /dev/null +++ b/plugins/gcp-projects/api-report.md @@ -0,0 +1,86 @@ +## API Report File for "@backstage/plugin-gcp-projects" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { OAuthApi } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export type GcpApi = { + listProjects(): Promise; + getProject(projectId: string): Promise; + createProject(options: { + projectId: string; + projectName: string; + }): Promise; +}; + +// @public (undocumented) +export const gcpApiRef: ApiRef; + +// @public (undocumented) +export class GcpClient implements GcpApi { + constructor(googleAuthApi: OAuthApi); + // (undocumented) + createProject(options: { + projectId: string; + projectName: string; + }): Promise; + // (undocumented) + getProject(projectId: string): Promise; + // (undocumented) + getToken(): Promise; + // (undocumented) + listProjects(): Promise; +} + +// @public (undocumented) +export const GcpProjectsPage: () => JSX.Element; + +// @public (undocumented) +const gcpProjectsPlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + +export { gcpProjectsPlugin } + +export { gcpProjectsPlugin as plugin } + +// @public (undocumented) +export type Operation = { + name: string; + metadata: string; + done: boolean; + error: Status; + response: string; +}; + +// @public (undocumented) +export type Project = { + name: string; + projectNumber?: string; + projectId: string; + lifecycleState?: string; + createTime?: string; +}; + +// @public (undocumented) +export type ProjectDetails = { + details: string; +}; + +// @public (undocumented) +export type Status = { + code: number; + message: string; + details: string[]; +}; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/git-release-manager/api-report.md b/plugins/git-release-manager/api-report.md new file mode 100644 index 0000000000..d898f83dd6 --- /dev/null +++ b/plugins/git-release-manager/api-report.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-git-release-manager" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const gitReleaseManagerApiRef: ApiRef; + +// @public (undocumented) +export const GitReleaseManagerPage: GitReleaseManager; + +// @public (undocumented) +export const gitReleaseManagerPlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/github-actions/api-report.md b/plugins/github-actions/api-report.md new file mode 100644 index 0000000000..1795bbdbea --- /dev/null +++ b/plugins/github-actions/api-report.md @@ -0,0 +1,213 @@ +## API Report File for "@backstage/plugin-github-actions" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { ConfigApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core'; +import { OAuthApi } from '@backstage/core'; +import { RestEndpointMethodTypes } from '@octokit/rest'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export enum BuildStatus { + // (undocumented) + 'failure' = 1, + // (undocumented) + 'pending' = 2, + // (undocumented) + 'running' = 3, + // (undocumented) + 'success' = 0 +} + +// @public (undocumented) +export const EntityGithubActionsContent: (_props: { + entity?: Entity| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityLatestGithubActionRunCard: ({ branch, variant, }: { + entity?: Entity| undefined; + branch: string; + variant?: InfoCardVariants| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityLatestGithubActionsForBranchCard: ({ branch, variant, }: { + entity?: Entity| undefined; + branch: string; + variant?: InfoCardVariants| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityRecentGithubActionsRunsCard: ({ branch, dense, limit, variant, }: Props) => JSX.Element; + +// @public (undocumented) +export const GITHUB_ACTIONS_ANNOTATION = "github.com/project-slug"; + +// @public (undocumented) +export type GithubActionsApi = { + listWorkflowRuns: ({ hostname, owner, repo, pageSize, page, branch, }: { + hostname?: string; + owner: string; + repo: string; + pageSize?: number; + page?: number; + branch?: string; + }) => Promise; + getWorkflow: ({ hostname, owner, repo, id, }: { + hostname?: string; + owner: string; + repo: string; + id: number; + }) => Promise; + getWorkflowRun: ({ hostname, owner, repo, id, }: { + hostname?: string; + owner: string; + repo: string; + id: number; + }) => Promise; + reRunWorkflow: ({ hostname, owner, repo, runId, }: { + hostname?: string; + owner: string; + repo: string; + runId: number; + }) => Promise; + listJobsForWorkflowRun: ({ hostname, owner, repo, id, pageSize, page, }: { + hostname?: string; + owner: string; + repo: string; + id: number; + pageSize?: number; + page?: number; + }) => Promise; + downloadJobLogsForWorkflowRun: ({ hostname, owner, repo, runId, }: { + hostname?: string; + owner: string; + repo: string; + runId: number; + }) => Promise; +}; + +// @public (undocumented) +export const githubActionsApiRef: ApiRef; + +// @public (undocumented) +export class GithubActionsClient implements GithubActionsApi { + constructor(options: { + configApi: ConfigApi; + githubAuthApi: OAuthApi; + }); + // (undocumented) + downloadJobLogsForWorkflowRun({ hostname, owner, repo, runId, }: { + hostname?: string; + owner: string; + repo: string; + runId: number; + }): Promise; + // (undocumented) + getWorkflow({ hostname, owner, repo, id, }: { + hostname?: string; + owner: string; + repo: string; + id: number; + }): Promise; + // (undocumented) + getWorkflowRun({ hostname, owner, repo, id, }: { + hostname?: string; + owner: string; + repo: string; + id: number; + }): Promise; + // (undocumented) + listJobsForWorkflowRun({ hostname, owner, repo, id, pageSize, page, }: { + hostname?: string; + owner: string; + repo: string; + id: number; + pageSize?: number; + page?: number; + }): Promise; + // (undocumented) + listWorkflowRuns({ hostname, owner, repo, pageSize, page, branch, }: { + hostname?: string; + owner: string; + repo: string; + pageSize?: number; + page?: number; + branch?: string; + }): Promise; + // (undocumented) + reRunWorkflow({ hostname, owner, repo, runId, }: { + hostname?: string; + owner: string; + repo: string; + runId: number; + }): Promise; +} + +// @public (undocumented) +const githubActionsPlugin: BackstagePlugin<{ + entityContent: RouteRef; +}, {}>; + +export { githubActionsPlugin } + +export { githubActionsPlugin as plugin } + +// @public (undocumented) +const isGithubActionsAvailable: (entity: Entity) => boolean; + +export { isGithubActionsAvailable } + +export { isGithubActionsAvailable as isPluginApplicableToEntity } + +// @public (undocumented) +export type Job = { + html_url: string; + status: string; + conclusion: string; + started_at: string; + completed_at: string; + id: number; + name: string; + steps: Step[]; +}; + +// @public (undocumented) +export type Jobs = { + total_count: number; + jobs: Job[]; +}; + +// @public (undocumented) +export const LatestWorkflowRunCard: ({ branch, variant, }: Props_3) => JSX.Element; + +// @public (undocumented) +export const LatestWorkflowsForBranchCard: ({ branch, variant, }: Props_3) => JSX.Element; + +// @public (undocumented) +export const RecentWorkflowRunsCard: ({ branch, dense, limit, variant, }: Props) => JSX.Element; + +// @public (undocumented) +export const Router: (_props: Props_2) => JSX.Element; + +// @public (undocumented) +export type Step = { + name: string; + status: string; + conclusion?: string; + number: number; + started_at: string; + completed_at: string; +}; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/gitops-profiles/api-report.md b/plugins/gitops-profiles/api-report.md new file mode 100644 index 0000000000..aad08f2b43 --- /dev/null +++ b/plugins/gitops-profiles/api-report.md @@ -0,0 +1,197 @@ +## API Report File for "@backstage/plugin-gitops-profiles" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export interface ApplyProfileRequest { + // (undocumented) + gitHubToken: string; + // (undocumented) + gitHubUser: string; + // (undocumented) + profiles: string[]; + // (undocumented) + targetOrg: string; + // (undocumented) + targetRepo: string; +} + +// @public (undocumented) +export interface ChangeClusterStateRequest { + // (undocumented) + clusterState: 'present' | 'absent'; + // (undocumented) + gitHubToken: string; + // (undocumented) + gitHubUser: string; + // (undocumented) + targetOrg: string; + // (undocumented) + targetRepo: string; +} + +// @public (undocumented) +export interface CloneFromTemplateRequest { + // (undocumented) + gitHubToken: string; + // (undocumented) + gitHubUser: string; + // (undocumented) + secrets: { + awsAccessKeyId: string; + awsSecretAccessKey: string; + }; + // (undocumented) + targetOrg: string; + // (undocumented) + targetRepo: string; + // (undocumented) + templateRepository: string; +} + +// @public (undocumented) +export interface ClusterStatus { + // (undocumented) + conclusion: string; + // (undocumented) + link: string; + // (undocumented) + name: string; + // (undocumented) + runStatus: Status[]; + // (undocumented) + status: string; +} + +// @public (undocumented) +export class FetchError extends Error { + // (undocumented) + static forResponse(resp: Response): Promise; + // (undocumented) + get name(): string; +} + +// @public (undocumented) +export interface GithubUserInfoRequest { + // (undocumented) + accessToken: string; +} + +// @public (undocumented) +export interface GithubUserInfoResponse { + // (undocumented) + login: string; +} + +// @public (undocumented) +export type GitOpsApi = { + url: string; + fetchLog(req: PollLogRequest): Promise; + changeClusterState(req: ChangeClusterStateRequest): Promise; + cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise; + applyProfiles(req: ApplyProfileRequest): Promise; + listClusters(req: ListClusterRequest): Promise; + fetchUserInfo(req: GithubUserInfoRequest): Promise; +}; + +// @public (undocumented) +export const gitOpsApiRef: ApiRef; + +// @public (undocumented) +export const GitopsProfilesClusterListPage: () => JSX.Element; + +// @public (undocumented) +export const GitopsProfilesClusterPage: () => JSX.Element; + +// @public (undocumented) +export const GitopsProfilesCreatePage: () => JSX.Element; + +// @public (undocumented) +const gitopsProfilesPlugin: BackstagePlugin<{ + listPage: RouteRef; + detailsPage: RouteRef<{ + owner: string; + repo: string; + }>; + createPage: RouteRef; +}, {}>; + +export { gitopsProfilesPlugin } + +export { gitopsProfilesPlugin as plugin } + +// @public (undocumented) +export class GitOpsRestApi implements GitOpsApi { + constructor(url?: string); + // (undocumented) + applyProfiles(req: ApplyProfileRequest): Promise; + // (undocumented) + changeClusterState(req: ChangeClusterStateRequest): Promise; + // (undocumented) + cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise; + // (undocumented) + fetchLog(req: PollLogRequest): Promise; + // (undocumented) + fetchUserInfo(req: GithubUserInfoRequest): Promise; + // (undocumented) + listClusters(req: ListClusterRequest): Promise; + // (undocumented) + url: string; +} + +// @public (undocumented) +export interface ListClusterRequest { + // (undocumented) + gitHubToken: string; + // (undocumented) + gitHubUser: string; +} + +// @public (undocumented) +export interface ListClusterStatusesResponse { + // (undocumented) + result: ClusterStatus[]; +} + +// @public (undocumented) +export interface PollLogRequest { + // (undocumented) + gitHubToken: string; + // (undocumented) + gitHubUser: string; + // (undocumented) + targetOrg: string; + // (undocumented) + targetRepo: string; +} + +// @public (undocumented) +export interface Status { + // (undocumented) + conclusion: string; + // (undocumented) + message: string; + // (undocumented) + status: string; +} + +// @public (undocumented) +export interface StatusResponse { + // (undocumented) + link: string; + // (undocumented) + result: Status[]; + // (undocumented) + status: string; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/graphiql/api-report.md b/plugins/graphiql/api-report.md new file mode 100644 index 0000000000..7c73b6a985 --- /dev/null +++ b/plugins/graphiql/api-report.md @@ -0,0 +1,82 @@ +## API Report File for "@backstage/plugin-graphiql" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { ErrorApi } from '@backstage/core-plugin-api'; +import { IconComponent } from '@backstage/core'; +import { OAuthApi } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export type EndpointConfig = { + id: string; + title: string; + url: string; + method?: 'POST'; + headers?: { + [name in string]: string; + }; +}; + +// @public (undocumented) +export type GithubEndpointConfig = { + id: string; + title: string; + url?: string; + errorApi?: ErrorApi; + githubAuthApi: OAuthApi; +}; + +// @public (undocumented) +export const GraphiQLIcon: IconComponent; + +// @public (undocumented) +export const GraphiQLPage: () => JSX.Element; + +// @public (undocumented) +const graphiqlPlugin: BackstagePlugin<{}, {}>; + +export { graphiqlPlugin } + +export { graphiqlPlugin as plugin } + +// @public (undocumented) +export const graphiQLRouteRef: RouteRef; + +// @public (undocumented) +export type GraphQLBrowseApi = { + getEndpoints(): Promise; +}; + +// @public (undocumented) +export const graphQlBrowseApiRef: ApiRef; + +// @public (undocumented) +export type GraphQLEndpoint = { + id: string; + title: string; + fetcher: (body: any) => Promise; +}; + +// @public (undocumented) +export class GraphQLEndpoints implements GraphQLBrowseApi { + // (undocumented) + static create(config: EndpointConfig): GraphQLEndpoint; + // (undocumented) + static from(endpoints: GraphQLEndpoint[]): GraphQLEndpoints; + // (undocumented) + getEndpoints(): Promise; + static github(config: GithubEndpointConfig): GraphQLEndpoint; +} + +// @public (undocumented) +export const Router: () => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/graphql/api-report.md b/plugins/graphql/api-report.md new file mode 100644 index 0000000000..8109c94122 --- /dev/null +++ b/plugins/graphql/api-report.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-graphql-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Config } from '@backstage/config'; +import express from 'express'; +import { Logger } from 'winston'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + config: Config; + // (undocumented) + logger: Logger; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/ilert/api-report.md b/plugins/ilert/api-report.md new file mode 100644 index 0000000000..f61b43b90b --- /dev/null +++ b/plugins/ilert/api-report.md @@ -0,0 +1,205 @@ +## API Report File for "@backstage/plugin-ilert" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { ConfigApi } from '@backstage/core'; +import { DiscoveryApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { IconComponent } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const EntityILertCard: () => JSX.Element; + +// @public (undocumented) +export type GetIncidentsCountOpts = { + states?: IncidentStatus[]; +}; + +// @public (undocumented) +export type GetIncidentsOpts = { + maxResults?: number; + startIndex?: number; + states?: IncidentStatus[]; + alertSources?: number[]; +}; + +// @public (undocumented) +export interface ILertApi { + // (undocumented) + acceptIncident(incident: Incident, userName: string): Promise; + // (undocumented) + addImmediateMaintenance(alertSourceId: number, minutes: number): Promise; + // (undocumented) + assignIncident(incident: Incident, responder: IncidentResponder): Promise; + // (undocumented) + createIncident(eventRequest: EventRequest): Promise; + // (undocumented) + disableAlertSource(alertSource: AlertSource): Promise; + // (undocumented) + enableAlertSource(alertSource: AlertSource): Promise; + // (undocumented) + fetchAlertSource(idOrIntegrationKey: number | string): Promise; + // (undocumented) + fetchAlertSourceOnCalls(alertSource: AlertSource): Promise; + // (undocumented) + fetchAlertSources(): Promise; + // (undocumented) + fetchIncident(id: number): Promise; + // (undocumented) + fetchIncidentActions(incident: Incident): Promise; + // (undocumented) + fetchIncidentResponders(incident: Incident): Promise; + // (undocumented) + fetchIncidents(opts?: GetIncidentsOpts): Promise; + // (undocumented) + fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise; + // (undocumented) + fetchOnCallSchedules(): Promise; + // (undocumented) + fetchUptimeMonitor(id: number): Promise; + // (undocumented) + fetchUptimeMonitors(): Promise; + // (undocumented) + fetchUsers(): Promise; + // (undocumented) + getAlertSourceDetailsURL(alertSource: AlertSource | null): string; + // (undocumented) + getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string; + // (undocumented) + getIncidentDetailsURL(incident: Incident): string; + // (undocumented) + getScheduleDetailsURL(schedule: Schedule): string; + // (undocumented) + getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string; + // (undocumented) + getUserInitials(user: User | null): string; + // (undocumented) + getUserPhoneNumber(user: User | null): string; + // (undocumented) + overrideShift(scheduleId: number, userId: number, start: string, end: string): Promise; + // (undocumented) + pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; + // (undocumented) + resolveIncident(incident: Incident, userName: string): Promise; + // (undocumented) + resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; + // (undocumented) + triggerIncidentAction(incident: Incident, action: IncidentAction): Promise; +} + +// @public (undocumented) +export const ilertApiRef: ApiRef; + +// @public (undocumented) +export const ILertCard: () => JSX.Element; + +// @public (undocumented) +export class ILertClient implements ILertApi { + constructor(opts: Options); + // (undocumented) + acceptIncident(incident: Incident, userName: string): Promise; + // (undocumented) + addImmediateMaintenance(alertSourceId: number, minutes: number): Promise; + // (undocumented) + assignIncident(incident: Incident, responder: IncidentResponder): Promise; + // (undocumented) + createIncident(eventRequest: EventRequest): Promise; + // (undocumented) + disableAlertSource(alertSource: AlertSource): Promise; + // (undocumented) + enableAlertSource(alertSource: AlertSource): Promise; + // (undocumented) + fetchAlertSource(idOrIntegrationKey: number | string): Promise; + // (undocumented) + fetchAlertSourceOnCalls(alertSource: AlertSource): Promise; + // (undocumented) + fetchAlertSources(): Promise; + // (undocumented) + fetchIncident(id: number): Promise; + // (undocumented) + fetchIncidentActions(incident: Incident): Promise; + // (undocumented) + fetchIncidentResponders(incident: Incident): Promise; + // (undocumented) + fetchIncidents(opts?: GetIncidentsOpts): Promise; + // (undocumented) + fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise; + // (undocumented) + fetchOnCallSchedules(): Promise; + // (undocumented) + fetchUptimeMonitor(id: number): Promise; + // (undocumented) + fetchUptimeMonitors(): Promise; + // (undocumented) + fetchUsers(): Promise; + // (undocumented) + static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi): ILertClient; + // (undocumented) + getAlertSourceDetailsURL(alertSource: AlertSource | null): string; + // (undocumented) + getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string; + // (undocumented) + getIncidentDetailsURL(incident: Incident): string; + // (undocumented) + getScheduleDetailsURL(schedule: Schedule): string; + // (undocumented) + getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string; + // (undocumented) + getUserInitials(user: User | null): string; + // (undocumented) + getUserPhoneNumber(user: User | null): string; + // (undocumented) + overrideShift(scheduleId: number, userId: number, start: string, end: string): Promise; + // (undocumented) + pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; + // (undocumented) + resolveIncident(incident: Incident, userName: string): Promise; + // (undocumented) + resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; + // (undocumented) + triggerIncidentAction(incident: Incident, action: IncidentAction): Promise; +} + +// @public (undocumented) +export const ILertIcon: IconComponent; + +// @public (undocumented) +export const ILertPage: () => JSX.Element; + +// @public (undocumented) +const ilertPlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + +export { ilertPlugin } + +export { ilertPlugin as plugin } + +// @public (undocumented) +export const iLertRouteRef: RouteRef; + +// @public (undocumented) +const isPluginApplicableToEntity: (entity: Entity) => boolean; + +export { isPluginApplicableToEntity as isILertAvailable } + +export { isPluginApplicableToEntity } + +// @public (undocumented) +export const Router: () => JSX.Element; + +// @public (undocumented) +export type TableState = { + page: number; + pageSize: number; +}; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/jenkins/api-report.md b/plugins/jenkins/api-report.md new file mode 100644 index 0000000000..4c88aab70c --- /dev/null +++ b/plugins/jenkins/api-report.md @@ -0,0 +1,83 @@ +## API Report File for "@backstage/plugin-jenkins" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { DiscoveryApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const EntityJenkinsContent: (_props: { + entity?: Entity| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityLatestJenkinsRunCard: ({ branch, variant, }: { + branch: string; + variant?: InfoCardVariants| undefined; +}) => JSX.Element; + +// @public (undocumented) +const isJenkinsAvailable: (entity: Entity) => boolean; + +export { isJenkinsAvailable } + +export { isJenkinsAvailable as isPluginApplicableToEntity } + +// @public (undocumented) +export const JENKINS_ANNOTATION = "jenkins.io/github-folder"; + +// @public (undocumented) +export class JenkinsApi { + constructor(options: Options); + // (undocumented) + extractJobDetailsFromBuildName(buildName: string): { + jobName: string; + buildNumber: number; + }; + // (undocumented) + extractScmDetailsFromJob(jobDetails: any): any | undefined; + // (undocumented) + getBuild(buildName: string): Promise; + // (undocumented) + getFolder(folderName: string): Promise; + // (undocumented) + getJob(jobName: string): Promise; + // (undocumented) + getLastBuild(jobName: string): Promise; + // (undocumented) + mapJenkinsBuildToCITable(jenkinsResult: any, jobScmInfo?: any): CITableBuildInfo; + // (undocumented) + retry(buildName: string): Promise; +} + +// @public (undocumented) +export const jenkinsApiRef: ApiRef; + +// @public (undocumented) +const jenkinsPlugin: BackstagePlugin<{ + entityContent: RouteRef; +}, {}>; + +export { jenkinsPlugin } + +export { jenkinsPlugin as plugin } + +// @public (undocumented) +export const LatestRunCard: ({ branch, variant, }: { + branch: string; + variant?: InfoCardVariants | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const Router: (_props: Props) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/kafka-backend/api-report.md b/plugins/kafka-backend/api-report.md new file mode 100644 index 0000000000..47754ad572 --- /dev/null +++ b/plugins/kafka-backend/api-report.md @@ -0,0 +1,17 @@ +## API Report File for "@backstage/plugin-kafka-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Config } from '@backstage/config'; +import express from 'express'; +import { Logger } from 'winston'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/kafka/api-report.md b/plugins/kafka/api-report.md new file mode 100644 index 0000000000..acb213fd53 --- /dev/null +++ b/plugins/kafka/api-report.md @@ -0,0 +1,41 @@ +## API Report File for "@backstage/plugin-kafka" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const EntityKafkaContent: (_props: { + entity?: Entity| undefined; +}) => JSX.Element; + +// @public (undocumented) +const isPluginApplicableToEntity: (entity: Entity) => boolean; + +export { isPluginApplicableToEntity as isKafkaAvailable } + +export { isPluginApplicableToEntity } + +// @public (undocumented) +export const KAFKA_CONSUMER_GROUP_ANNOTATION = "kafka.apache.org/consumer-groups"; + +// @public (undocumented) +const kafkaPlugin: BackstagePlugin<{ + entityContent: RouteRef; +}, {}>; + +export { kafkaPlugin } + +export { kafkaPlugin as plugin } + +// @public (undocumented) +export const Router: (_props: Props) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md new file mode 100644 index 0000000000..fe43e8888a --- /dev/null +++ b/plugins/kubernetes-backend/api-report.md @@ -0,0 +1,103 @@ +## API Report File for "@backstage/plugin-kubernetes-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Config } from '@backstage/config'; +import express from 'express'; +import { FetchResponse } from '@backstage/plugin-kubernetes-common'; +import { KubernetesFetchError } from '@backstage/plugin-kubernetes-common'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { Logger } from 'winston'; + +// @public (undocumented) +export interface ClusterDetails { + // (undocumented) + authProvider: string; + // (undocumented) + name: string; + // (undocumented) + serviceAccountToken?: string | undefined; + // (undocumented) + skipTLSVerify?: boolean; + // (undocumented) + url: string; +} + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export interface CustomResource { + // (undocumented) + apiVersion: string; + // (undocumented) + group: string; + // (undocumented) + plural: string; +} + +// @public (undocumented) +export interface FetchResponseWrapper { + // (undocumented) + errors: KubernetesFetchError[]; + // (undocumented) + responses: FetchResponse[]; +} + +// @public (undocumented) +export interface KubernetesClustersSupplier { + // (undocumented) + getClusters(): Promise; +} + +// @public (undocumented) +export interface KubernetesFetcher { + // (undocumented) + fetchObjectsForService(params: ObjectFetchParams): Promise; +} + +// @public (undocumented) +export type KubernetesObjectTypes = 'pods' | 'services' | 'configmaps' | 'deployments' | 'replicasets' | 'horizontalpodautoscalers' | 'ingresses' | 'customresources'; + +// @public (undocumented) +export interface KubernetesServiceLocator { + // (undocumented) + getClustersByServiceId(serviceId: string): Promise; +} + +// @public (undocumented) +export const makeRouter: (logger: Logger, kubernetesFanOutHandler: KubernetesFanOutHandler, clusterDetails: ClusterDetails[]) => express.Router; + +// @public (undocumented) +export interface ObjectFetchParams { + // (undocumented) + clusterDetails: ClusterDetails; + // (undocumented) + customResources: CustomResource[]; + // (undocumented) + labelSelector: string; + // (undocumented) + objectTypesToFetch: Set; + // (undocumented) + serviceId: string; +} + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + clusterSupplier?: KubernetesClustersSupplier; + // (undocumented) + config: Config; + // (undocumented) + logger: Logger; +} + +// @public (undocumented) +export type ServiceLocatorMethod = 'multiTenant' | 'http'; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md new file mode 100644 index 0000000000..a8abec517e --- /dev/null +++ b/plugins/kubernetes-common/api-report.md @@ -0,0 +1,130 @@ +## API Report File for "@backstage/plugin-kubernetes-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Entity } from '@backstage/catalog-model'; +import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node'; +import { V1ConfigMap } from '@kubernetes/client-node'; +import { V1Deployment } from '@kubernetes/client-node'; +import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; +import { V1Pod } from '@kubernetes/client-node'; +import { V1ReplicaSet } from '@kubernetes/client-node'; +import { V1Service } from '@kubernetes/client-node'; + +// @public (undocumented) +export type AuthProviderType = 'google' | 'serviceAccount' | 'aws'; + +// @public (undocumented) +export interface ClusterObjects { + // (undocumented) + cluster: { + name: string; + }; + // (undocumented) + errors: KubernetesFetchError[]; + // (undocumented) + resources: FetchResponse[]; +} + +// @public (undocumented) +export interface ConfigMapFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'configmaps'; +} + +// @public (undocumented) +export interface CustomResourceFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'customresources'; +} + +// @public (undocumented) +export interface DeploymentFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'deployments'; +} + +// @public (undocumented) +export type FetchResponse = PodFetchResponse | ServiceFetchResponse | ConfigMapFetchResponse | DeploymentFetchResponse | ReplicaSetsFetchResponse | HorizontalPodAutoscalersFetchResponse | IngressesFetchResponse | CustomResourceFetchResponse; + +// @public (undocumented) +export interface HorizontalPodAutoscalersFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'horizontalpodautoscalers'; +} + +// @public (undocumented) +export interface IngressesFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'ingresses'; +} + +// @public (undocumented) +export type KubernetesErrorTypes = 'BAD_REQUEST' | 'UNAUTHORIZED_ERROR' | 'SYSTEM_ERROR' | 'UNKNOWN_ERROR'; + +// @public (undocumented) +export interface KubernetesFetchError { + // (undocumented) + errorType: KubernetesErrorTypes; + // (undocumented) + resourcePath?: string; + // (undocumented) + statusCode?: number; +} + +// @public (undocumented) +export interface KubernetesRequestBody { + // (undocumented) + auth?: { + google?: string; + }; + // (undocumented) + entity: Entity; +} + +// @public (undocumented) +export interface ObjectsByEntityResponse { + // (undocumented) + items: ClusterObjects[]; +} + +// @public (undocumented) +export interface PodFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'pods'; +} + +// @public (undocumented) +export interface ReplicaSetsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'replicasets'; +} + +// @public (undocumented) +export interface ServiceFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'services'; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md new file mode 100644 index 0000000000..2f1b5155fd --- /dev/null +++ b/plugins/kubernetes/api-report.md @@ -0,0 +1,46 @@ +## API Report File for "@backstage/plugin-kubernetes" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { OAuthApi } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const EntityKubernetesContent: (_props: { + entity?: Entity| undefined; +}) => JSX.Element; + +// @public (undocumented) +export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { + constructor(options: { + googleAuthApi: OAuthApi; + }); + // (undocumented) + decorateRequestBodyForAuth(authProvider: string, requestBody: KubernetesRequestBody): Promise; + } + +// @public (undocumented) +export const kubernetesAuthProvidersApiRef: ApiRef; + +// @public (undocumented) +const kubernetesPlugin: BackstagePlugin<{ + entityContent: RouteRef; +}, {}>; + +export { kubernetesPlugin } + +export { kubernetesPlugin as plugin } + +// @public (undocumented) +export const Router: (_props: Props) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/lighthouse/api-report.md b/plugins/lighthouse/api-report.md new file mode 100644 index 0000000000..18d8358257 --- /dev/null +++ b/plugins/lighthouse/api-report.md @@ -0,0 +1,186 @@ +## API Report File for "@backstage/plugin-lighthouse" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export type Audit = AuditRunning | AuditFailed | AuditCompleted; + +// @public (undocumented) +export interface AuditCompleted extends AuditBase { + // (undocumented) + categories: Record; + // (undocumented) + report: Object; + // (undocumented) + status: 'COMPLETED'; + // (undocumented) + timeCompleted: string; +} + +// @public (undocumented) +export interface AuditFailed extends AuditBase { + // (undocumented) + status: 'FAILED'; + // (undocumented) + timeCompleted: string; +} + +// @public (undocumented) +export interface AuditRunning extends AuditBase { + // (undocumented) + status: 'RUNNING'; +} + +// @public (undocumented) +export const EmbeddedRouter: (_props: Props) => JSX.Element; + +// @public (undocumented) +export const EntityLastLighthouseAuditCard: ({ dense, variant, }: { + dense?: boolean | undefined; + variant?: InfoCardVariants| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityLighthouseContent: (_props: { + entity?: Entity| undefined; +}) => JSX.Element; + +// @public (undocumented) +export class FetchError extends Error { + // (undocumented) + static forResponse(resp: Response): Promise; + // (undocumented) + get name(): string; +} + +// @public (undocumented) +const isLighthouseAvailable: (entity: Entity) => boolean; + +export { isLighthouseAvailable } + +export { isLighthouseAvailable as isPluginApplicableToEntity } + +// @public (undocumented) +export interface LASListRequest { + // (undocumented) + limit?: number; + // (undocumented) + offset?: number; +} + +// @public (undocumented) +export interface LASListResponse { + // (undocumented) + items: Item[]; + // (undocumented) + limit: number; + // (undocumented) + offset: number; + // (undocumented) + total: number; +} + +// @public (undocumented) +export const LastLighthouseAuditCard: ({ dense, variant, }: { + dense?: boolean | undefined; + variant?: InfoCardVariants | undefined; +}) => JSX.Element; + +// @public (undocumented) +export type LighthouseApi = { + url: string; + getWebsiteList: (listOptions: LASListRequest) => Promise; + getWebsiteForAuditId: (auditId: string) => Promise; + triggerAudit: (payload: TriggerAuditPayload) => Promise; + getWebsiteByUrl: (websiteUrl: string) => Promise; +}; + +// @public (undocumented) +export const lighthouseApiRef: ApiRef; + +// @public (undocumented) +export interface LighthouseCategoryAbbr { + // (undocumented) + id: LighthouseCategoryId; + // (undocumented) + score: number; + // (undocumented) + title: string; +} + +// @public (undocumented) +export type LighthouseCategoryId = 'pwa' | 'seo' | 'performance' | 'accessibility' | 'best-practices'; + +// @public (undocumented) +export const LighthousePage: () => JSX.Element; + +// @public (undocumented) +const lighthousePlugin: BackstagePlugin<{ + root: RouteRef; + entityContent: RouteRef; +}, {}>; + +export { lighthousePlugin } + +export { lighthousePlugin as plugin } + +// @public (undocumented) +export class LighthouseRestApi implements LighthouseApi { + constructor(url: string); + // (undocumented) + static fromConfig(config: Config): LighthouseRestApi; + // (undocumented) + getWebsiteByUrl(websiteUrl: string): Promise; + // (undocumented) + getWebsiteForAuditId(auditId: string): Promise; + // (undocumented) + getWebsiteList({ limit, offset, }?: LASListRequest): Promise; + // (undocumented) + triggerAudit(payload: TriggerAuditPayload): Promise; + // (undocumented) + url: string; +} + +// @public (undocumented) +export const Router: () => JSX.Element; + +// @public (undocumented) +export interface TriggerAuditPayload { + // (undocumented) + options: { + lighthouseConfig: { + settings: { + emulatedFormFactor: string; + }; + }; + }; + // (undocumented) + url: string; +} + +// @public (undocumented) +export interface Website { + // (undocumented) + audits: Audit[]; + // (undocumented) + lastAudit: Audit; + // (undocumented) + url: string; +} + +// @public (undocumented) +export type WebsiteListResponse = LASListResponse; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/newrelic/api-report.md b/plugins/newrelic/api-report.md new file mode 100644 index 0000000000..e472405e22 --- /dev/null +++ b/plugins/newrelic/api-report.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-newrelic" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const NewRelicPage: () => JSX.Element; + +// @public (undocumented) +const newRelicPlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + +export { newRelicPlugin } + +export { newRelicPlugin as plugin } + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md new file mode 100644 index 0000000000..cb3fa445f0 --- /dev/null +++ b/plugins/org/api-report.md @@ -0,0 +1,69 @@ +## API Report File for "@backstage/plugin-org" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { GroupEntity } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core'; +import { UserEntity } from '@backstage/catalog-model'; + +// @public (undocumented) +export const EntityGroupProfileCard: ({ variant, }: { + entity?: GroupEntity| undefined; + variant?: InfoCardVariants| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityMembersListCard: (_props: { + entity?: GroupEntity| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityOwnershipCard: ({ variant, }: { + entity?: Entity| undefined; + variant?: InfoCardVariants| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const EntityUserProfileCard: ({ variant, }: { + entity?: UserEntity| undefined; + variant?: InfoCardVariants| undefined; +}) => JSX.Element; + +// @public (undocumented) +export const GroupProfileCard: ({ variant, }: { + entity?: GroupEntity | undefined; + variant?: InfoCardVariants | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const MembersListCard: (_props: { + entity?: GroupEntity; +}) => JSX.Element; + +// @public (undocumented) +const orgPlugin: BackstagePlugin<{}, {}>; + +export { orgPlugin } + +export { orgPlugin as plugin } + +// @public (undocumented) +export const OwnershipCard: ({ variant, }: { + entity?: Entity | undefined; + variant?: InfoCardVariants | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const UserProfileCard: ({ variant, }: { + entity?: UserEntity | undefined; + variant?: InfoCardVariants | undefined; +}) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md new file mode 100644 index 0000000000..b3457ccb12 --- /dev/null +++ b/plugins/pagerduty/api-report.md @@ -0,0 +1,62 @@ +## API Report File for "@backstage/plugin-pagerduty" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { ConfigApi } from '@backstage/core'; +import { DiscoveryApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { PropsWithChildren } from 'react'; + +// @public (undocumented) +export const EntityPagerDutyCard: () => JSX.Element; + +// @public (undocumented) +const isPluginApplicableToEntity: (entity: Entity) => boolean; + +export { isPluginApplicableToEntity as isPagerDutyAvailable } + +export { isPluginApplicableToEntity } + +// @public (undocumented) +export const pagerDutyApiRef: ApiRef; + +// @public (undocumented) +export const PagerDutyCard: () => JSX.Element; + +// @public (undocumented) +export class PagerDutyClient implements PagerDutyApi { + constructor(config: ClientApiConfig); + // (undocumented) + static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi): PagerDutyClient; + // (undocumented) + getIncidentsByServiceId(serviceId: string): Promise; + // (undocumented) + getOnCallByPolicyId(policyId: string): Promise; + // (undocumented) + getServiceByIntegrationKey(integrationKey: string): Promise; + // (undocumented) + triggerAlarm({ integrationKey, source, description, userName, }: TriggerAlarmRequest): Promise; +} + +// @public (undocumented) +const pagerDutyPlugin: BackstagePlugin<{}, {}>; + +export { pagerDutyPlugin } + +export { pagerDutyPlugin as plugin } + +// @public (undocumented) +export function TriggerButton({ children, }: PropsWithChildren): JSX.Element; + +// @public (undocumented) +export class UnauthorizedError extends Error { +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md new file mode 100644 index 0000000000..1b1eba3b00 --- /dev/null +++ b/plugins/proxy-backend/api-report.md @@ -0,0 +1,18 @@ +## API Report File for "@backstage/plugin-proxy-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Config } from '@backstage/config'; +import express from 'express'; +import { Logger } from 'winston'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/register-component/api-report.md b/plugins/register-component/api-report.md new file mode 100644 index 0000000000..401031d0ac --- /dev/null +++ b/plugins/register-component/api-report.md @@ -0,0 +1,32 @@ +## API Report File for "@backstage/plugin-register-component" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const RegisterComponentPage: ({ catalogRouteRef, }: { + catalogRouteRef: RouteRef; +}) => JSX.Element; + +// @public (undocumented) +const registerComponentPlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + +export { registerComponentPlugin as plugin } + +export { registerComponentPlugin } + +// @public @deprecated +export const Router: ({ catalogRouteRef }: { + catalogRouteRef: RouteRef; +}) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/rollbar-backend/api-report.md b/plugins/rollbar-backend/api-report.md new file mode 100644 index 0000000000..fa8c67d4f0 --- /dev/null +++ b/plugins/rollbar-backend/api-report.md @@ -0,0 +1,60 @@ +## API Report File for "@backstage/plugin-rollbar-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Config } from '@backstage/config'; +import express from 'express'; +import { Logger } from 'winston'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export function getRequestHeaders(token: string): { + headers: { + 'X-Rollbar-Access-Token': string; + }; +}; + +// @public (undocumented) +export class RollbarApi { + constructor(accessToken: string, logger: Logger); + // (undocumented) + getActivatedCounts(projectName: string, options?: { + environment: string; + item_id?: number; + }): Promise; + // (undocumented) + getAllProjects(): Promise; + // (undocumented) + getOccuranceCounts(projectName: string, options?: { + environment: string; + item_id?: number; + }): Promise; + // (undocumented) + getProject(projectName: string): Promise; + // (undocumented) + getProjectItems(projectName: string): Promise; + // (undocumented) + getTopActiveItems(projectName: string, options?: { + hours: number; + environment: string; + }): Promise; + } + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + config: Config; + // (undocumented) + logger: Logger; + // (undocumented) + rollbarApi?: RollbarApi; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/rollbar/api-report.md b/plugins/rollbar/api-report.md new file mode 100644 index 0000000000..7049414389 --- /dev/null +++ b/plugins/rollbar/api-report.md @@ -0,0 +1,78 @@ +## API Report File for "@backstage/plugin-rollbar" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { DiscoveryApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { IdentityApi } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const EntityPageRollbar: (_props: Props) => JSX.Element; + +// @public (undocumented) +export const EntityRollbarContent: (_props: { + entity?: Entity| undefined; +}) => JSX.Element; + +// @public (undocumented) +const isPluginApplicableToEntity: (entity: Entity) => boolean; + +export { isPluginApplicableToEntity } + +export { isPluginApplicableToEntity as isRollbarAvailable } + +// @public (undocumented) +export const ROLLBAR_ANNOTATION = "rollbar.com/project-slug"; + +// @public (undocumented) +export interface RollbarApi { + // (undocumented) + getAllProjects(): Promise; + // (undocumented) + getProject(projectName: string): Promise; + // (undocumented) + getProjectItems(project: string): Promise; + // (undocumented) + getTopActiveItems(project: string, hours?: number): Promise; +} + +// @public (undocumented) +export const rollbarApiRef: ApiRef; + +// @public (undocumented) +export class RollbarClient implements RollbarApi { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }); + // (undocumented) + getAllProjects(): Promise; + // (undocumented) + getProject(projectName: string): Promise; + // (undocumented) + getProjectItems(project: string): Promise; + // (undocumented) + getTopActiveItems(project: string, hours?: number, environment?: string): Promise; + } + +// @public (undocumented) +const rollbarPlugin: BackstagePlugin<{ + entityContent: RouteRef; +}, {}>; + +export { rollbarPlugin as plugin } + +export { rollbarPlugin } + +// @public (undocumented) +export const Router: (_props: Props_2) => JSX.Element; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md new file mode 100644 index 0000000000..69c54343e3 --- /dev/null +++ b/plugins/scaffolder-backend/api-report.md @@ -0,0 +1,518 @@ +## API Report File for "@backstage/plugin-scaffolder-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AzureIntegrationConfig } from '@backstage/integration'; +import { BitbucketIntegrationConfig } from '@backstage/integration'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { ContainerRunner } from '@backstage/backend-common'; +import { createPullRequest } from 'octokit-plugin-create-pull-request'; +import express from 'express'; +import { GithubCredentialsProvider } from '@backstage/integration'; +import { GitHubIntegrationConfig } from '@backstage/integration'; +import { Gitlab } from '@gitbeaker/core'; +import { GitLabIntegrationConfig } from '@backstage/integration'; +import gitUrlParse from 'git-url-parse'; +import { JsonObject } from '@backstage/config'; +import { JsonValue } from '@backstage/config'; +import { Logger } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { Schema } from 'jsonschema'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { ScmIntegrations } from '@backstage/integration'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { UrlReader } from '@backstage/backend-common'; +import { Writable } from 'stream'; + +// @public (undocumented) +export type ActionContext = { + baseUrl?: string; + logger: Logger; + logStream: Writable; + token?: string | undefined; + workspacePath: string; + input: Input; + output(name: string, value: JsonValue): void; + createTemporaryDirectory(): Promise; +}; + +// @public (undocumented) +export class AzurePreparer implements PreparerBase { + constructor(config: { + token?: string; + }); + // (undocumented) + static fromConfig(config: AzureIntegrationConfig): AzurePreparer; + // (undocumented) + prepare({ url, workspacePath, logger }: PreparerOptions): Promise; +} + +// @public (undocumented) +export class AzurePublisher implements PublisherBase { + constructor(config: { + token: string; + }); + // (undocumented) + static fromConfig(config: AzureIntegrationConfig): Promise; + // (undocumented) + publish({ values, workspacePath, logger, }: PublisherOptions): Promise; +} + +// @public (undocumented) +export class BitbucketPreparer implements PreparerBase { + constructor(config: { + username?: string; + token?: string; + appPassword?: string; + }); + // (undocumented) + static fromConfig(config: BitbucketIntegrationConfig): BitbucketPreparer; + // (undocumented) + prepare({ url, workspacePath, logger }: PreparerOptions): Promise; +} + +// @public (undocumented) +export class BitbucketPublisher implements PublisherBase { + constructor(config: { + host: string; + token?: string; + appPassword?: string; + username?: string; + apiBaseUrl?: string; + repoVisibility: RepoVisibilityOptions_2; + }); + // (undocumented) + static fromConfig(config: BitbucketIntegrationConfig, { repoVisibility }: { + repoVisibility: RepoVisibilityOptions_2; + }): Promise; + // (undocumented) + publish({ values, workspacePath, logger, }: PublisherOptions): Promise; +} + +// @public +export class CatalogEntityClient { + constructor(catalogClient: CatalogApi); + findTemplate(templateName: string, options?: { + token?: string; + }): Promise; +} + +// @public (undocumented) +export class CookieCutter implements TemplaterBase { + constructor({ containerRunner }: { + containerRunner: ContainerRunner; + }); + // (undocumented) + run({ workspacePath, values, logStream, }: TemplaterRunOptions): Promise; +} + +// @public (undocumented) +export const createBuiltinActions: (options: { + reader: UrlReader; + integrations: ScmIntegrations; + catalogClient: CatalogApi; + templaters: TemplaterBuilder; +}) => TemplateAction[]; + +// @public (undocumented) +export function createCatalogRegisterAction(options: { + catalogClient: CatalogApi; + integrations: ScmIntegrations; +}): TemplateAction; + +// @public +export function createDebugLogAction(): TemplateAction; + +// @public (undocumented) +export function createFetchCookiecutterAction(options: { + reader: UrlReader; + integrations: ScmIntegrations; + templaters: TemplaterBuilder; +}): TemplateAction; + +// @public (undocumented) +export function createFetchPlainAction(options: { + reader: UrlReader; + integrations: ScmIntegrations; +}): TemplateAction; + +// @public (undocumented) +export function createLegacyActions(options: Options): TemplateAction[]; + +// @public (undocumented) +export function createPublishAzureAction(options: { + integrations: ScmIntegrationRegistry; +}): TemplateAction; + +// @public (undocumented) +export function createPublishBitbucketAction(options: { + integrations: ScmIntegrationRegistry; +}): TemplateAction; + +// @public +export function createPublishFileAction(): TemplateAction; + +// @public (undocumented) +export function createPublishGithubAction(options: { + integrations: ScmIntegrationRegistry; +}): TemplateAction; + +// @public (undocumented) +export const createPublishGithubPullRequestAction: ({ integrations, clientFactory, }: CreateGithubPullRequestActionOptions) => TemplateAction; + +// @public (undocumented) +export function createPublishGitlabAction(options: { + integrations: ScmIntegrationRegistry; +}): TemplateAction; + +// @public (undocumented) +export class CreateReactAppTemplater implements TemplaterBase { + constructor({ containerRunner }: { + containerRunner: ContainerRunner; + }); + // (undocumented) + run({ workspacePath, values, logStream, }: TemplaterRunOptions): Promise; +} + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export const createTemplateAction: | undefined; +}>>(templateAction: TemplateAction) => TemplateAction; + +// @public (undocumented) +export class FilePreparer implements PreparerBase { + // (undocumented) + prepare({ url, workspacePath }: PreparerOptions): Promise; +} + +// @public +export const getTemplaterKey: (entity: TemplateEntityV1alpha1) => string; + +// @public (undocumented) +export class GithubPreparer implements PreparerBase { + constructor(config: { + credentialsProvider: GithubCredentialsProvider; + }); + // (undocumented) + static fromConfig(config: GitHubIntegrationConfig): GithubPreparer; + // (undocumented) + prepare({ url, workspacePath, logger }: PreparerOptions): Promise; +} + +// @public @deprecated (undocumented) +export class GithubPublisher implements PublisherBase { + constructor(config: { + credentialsProvider: GithubCredentialsProvider; + repoVisibility: RepoVisibilityOptions; + apiBaseUrl: string | undefined; + }); + // (undocumented) + static fromConfig(config: GitHubIntegrationConfig, { repoVisibility }: { + repoVisibility: RepoVisibilityOptions; + }): Promise; + // (undocumented) + publish({ values, workspacePath, logger, }: PublisherOptions): Promise; +} + +// @public (undocumented) +export class GitlabPreparer implements PreparerBase { + constructor(config: { + token?: string; + }); + // (undocumented) + static fromConfig(config: GitLabIntegrationConfig): GitlabPreparer; + // (undocumented) + prepare({ url, workspacePath, logger }: PreparerOptions): Promise; +} + +// @public (undocumented) +export class GitlabPublisher implements PublisherBase { + constructor(config: { + token: string; + client: Gitlab; + repoVisibility: RepoVisibilityOptions_3; + }); + // (undocumented) + static fromConfig(config: GitLabIntegrationConfig, { repoVisibility }: { + repoVisibility: RepoVisibilityOptions_3; + }): Promise; + // (undocumented) + publish({ values, workspacePath, logger, }: PublisherOptions): Promise; +} + +// @public (undocumented) +export type Job = { + id: string; + context: StageContext; + status: ProcessorStatus; + stages: StageResult[]; + error?: Error; +}; + +// @public (undocumented) +export type JobAndDirectoryTuple = { + job: Job; + directory: string; +}; + +// @public (undocumented) +export class JobProcessor implements Processor { + constructor(workingDirectory: string); + // (undocumented) + create({ entity, values, stages, }: { + entity: TemplateEntityV1alpha1; + values: TemplaterValues; + stages: StageInput[]; + }): Job; + // (undocumented) + static fromConfig({ config, logger, }: { + config: Config; + logger: Logger; + }): Promise; + // (undocumented) + get(id: string): Job | undefined; + // (undocumented) + run(job: Job): Promise; + } + +// @public (undocumented) +export function joinGitUrlPath(repoUrl: string, path?: string): string; + +// @public (undocumented) +export type ParsedLocationAnnotation = { + protocol: 'file' | 'url'; + location: string; +}; + +// @public (undocumented) +export const parseLocationAnnotation: (entity: TemplateEntityV1alpha1) => ParsedLocationAnnotation; + +// @public (undocumented) +export interface PreparerBase { + prepare(opts: PreparerOptions): Promise; +} + +// @public (undocumented) +export type PreparerBuilder = { + register(host: string, preparer: PreparerBase): void; + get(url: string): PreparerBase; +}; + +// @public (undocumented) +export type PreparerOptions = { + url: string; + workspacePath: string; + logger: Logger; +}; + +// @public (undocumented) +export class Preparers implements PreparerBuilder { + // (undocumented) + static fromConfig(config: Config, _: { + logger: Logger; + }): Promise; + // (undocumented) + get(url: string): PreparerBase; + // (undocumented) + register(host: string, preparer: PreparerBase): void; +} + +// @public (undocumented) +export type Processor = { + create({ entity, values, stages, }: { + entity: TemplateEntityV1alpha1; + values: TemplaterValues; + stages: StageInput[]; + }): Job; + get(id: string): Job | undefined; + run(job: Job): Promise; +}; + +// @public (undocumented) +export type ProcessorStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; + +// @public +export type PublisherBase = { + publish(opts: PublisherOptions): Promise; +}; + +// @public (undocumented) +export type PublisherBuilder = { + register(host: string, publisher: PublisherBase): void; + get(storePath: string): PublisherBase; +}; + +// @public (undocumented) +export type PublisherOptions = { + values: TemplaterValues; + workspacePath: string; + logger: Logger; +}; + +// @public (undocumented) +export type PublisherResult = { + remoteUrl: string; + catalogInfoUrl?: string; +}; + +// @public (undocumented) +export class Publishers implements PublisherBuilder { + // (undocumented) + static fromConfig(config: Config, _options: { + logger: Logger; + }): Promise; + // (undocumented) + get(url: string): PublisherBase; + // (undocumented) + register(host: string, preparer: PublisherBase | undefined): void; +} + +// @public (undocumented) +export type RepoVisibilityOptions = 'private' | 'internal' | 'public'; + +// @public +export type RequiredTemplateValues = { + owner: string; + storePath: string; + destination?: { + git?: gitUrlParse.GitUrl; + }; +}; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + actions?: TemplateAction[]; + // (undocumented) + catalogClient: CatalogApi; + // (undocumented) + config: Config; + // (undocumented) + database: PluginDatabaseManager; + // (undocumented) + logger: Logger; + // (undocumented) + preparers: PreparerBuilder; + // (undocumented) + publishers: PublisherBuilder; + // (undocumented) + reader: UrlReader; + // (undocumented) + taskWorkers?: number; + // (undocumented) + templaters: TemplaterBuilder; +} + +// @public (undocumented) +export const runCommand: ({ command, args, logStream, }: RunCommandOptions) => Promise; + +// @public (undocumented) +export type RunCommandOptions = { + command: string; + args: string[]; + logStream?: Writable; +}; + +// @public (undocumented) +export type StageContext = { + values: TemplaterValues; + entity: TemplateEntityV1alpha1; + logger: Logger; + logStream: Writable; + workspacePath: string; +} & T; + +// @public (undocumented) +export interface StageInput { + // (undocumented) + handler(ctx: StageContext): Promise; + // (undocumented) + name: string; +} + +// @public (undocumented) +export interface StageResult extends StageInput { + // (undocumented) + endedAt?: number; + // (undocumented) + log: string[]; + // (undocumented) + startedAt?: number; + // (undocumented) + status: ProcessorStatus; +} + +// @public +export type SupportedTemplatingKey = 'cookiecutter' | string; + +// @public (undocumented) +export type TemplateAction = { + id: string; + description?: string; + schema?: { + input?: Schema; + output?: Schema; + }; + handler: (ctx: ActionContext) => Promise; +}; + +// @public (undocumented) +export class TemplateActionRegistry { + // (undocumented) + get(actionId: string): TemplateAction; + // (undocumented) + list(): TemplateAction[]; + // (undocumented) + register(action: TemplateAction): void; +} + +// @public (undocumented) +export type TemplaterBase = { + run(opts: TemplaterRunOptions): Promise; +}; + +// @public +export type TemplaterBuilder = { + register(protocol: SupportedTemplatingKey, templater: TemplaterBase): void; + get(templater: string): TemplaterBase; +}; + +// @public (undocumented) +export type TemplaterConfig = { + templater?: TemplaterBase; +}; + +// @public +export type TemplaterRunOptions = { + workspacePath: string; + values: TemplaterValues; + logStream?: Writable; +}; + +// @public +export type TemplaterRunResult = { + resultDir: string; +}; + +// @public (undocumented) +export class Templaters implements TemplaterBuilder { + // (undocumented) + get(templaterId: string): TemplaterBase; + // (undocumented) + register(templaterKey: SupportedTemplatingKey, templater: TemplaterBase): void; + } + +// @public (undocumented) +export type TemplaterValues = RequiredTemplateValues & Record; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md new file mode 100644 index 0000000000..85c2142d5e --- /dev/null +++ b/plugins/scaffolder/api-report.md @@ -0,0 +1,112 @@ +## API Report File for "@backstage/plugin-scaffolder" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { DiscoveryApi } from '@backstage/core'; +import { EntityName } from '@backstage/catalog-model'; +import { Extension } from '@backstage/core'; +import { ExternalRouteRef } from '@backstage/core'; +import { FieldProps } from '@rjsf/core'; +import { FieldValidation } from '@rjsf/core'; +import { IdentityApi } from '@backstage/core'; +import { JsonObject } from '@backstage/config'; +import { JSONSchema } from '@backstage/catalog-model'; +import { JsonValue } from '@backstage/config'; +import { Observable } from '@backstage/core'; +import { default as React_2 } from 'react'; +import { RouteRef } from '@backstage/core'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +// @public (undocumented) +export function createScaffolderFieldExtension(options: FieldExtensionOptions): Extension<() => null>; + +// @public (undocumented) +export const EntityPickerFieldExtension: () => null; + +// @public (undocumented) +export const OwnerPickerFieldExtension: () => null; + +// @public (undocumented) +export const RepoUrlPickerFieldExtension: () => null; + +// @public (undocumented) +export interface ScaffolderApi { + // (undocumented) + getIntegrationsList(options: { + allowedHosts: string[]; + }): Promise<{ + type: string; + title: string; + host: string; + }[]>; + // (undocumented) + getTask(taskId: string): Promise; + // (undocumented) + getTemplateParameterSchema(templateName: EntityName): Promise; + // (undocumented) + listActions(): Promise; + scaffold(templateName: string, values: Record): Promise; + // (undocumented) + streamLogs({ taskId, after, }: { + taskId: string; + after?: number; + }): Observable; +} + +// @public (undocumented) +export const scaffolderApiRef: ApiRef; + +// @public (undocumented) +export class ScaffolderClient implements ScaffolderApi { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + scmIntegrationsApi: ScmIntegrationRegistry; + }); + // (undocumented) + getIntegrationsList(options: { + allowedHosts: string[]; + }): Promise<{ + type: string; + title: string; + host: string; + }[]>; + // (undocumented) + getTask(taskId: string): Promise; + // (undocumented) + getTemplateParameterSchema(templateName: EntityName): Promise; + // (undocumented) + listActions(): Promise; + scaffold(templateName: string, values: Record): Promise; + // (undocumented) + streamLogs({ taskId, after, }: { + taskId: string; + after?: number; + }): Observable; +} + +// @public (undocumented) +export const ScaffolderFieldExtensions: React_2.ComponentType; + +// @public (undocumented) +export const ScaffolderPage: () => JSX.Element; + +// @public (undocumented) +const scaffolderPlugin: BackstagePlugin<{ + root: RouteRef; +}, { + registerComponent: ExternalRouteRef; +}>; + +export { scaffolderPlugin as plugin } + +export { scaffolderPlugin } + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md new file mode 100644 index 0000000000..03585b6b99 --- /dev/null +++ b/plugins/search-backend-node/api-report.md @@ -0,0 +1,68 @@ +## API Report File for "@backstage/plugin-search-backend-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { DocumentCollator } from '@backstage/search-common'; +import { DocumentDecorator } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/search-common'; +import { Logger } from 'winston'; +import { default as lunr_2 } from 'lunr'; +import { SearchQuery } from '@backstage/search-common'; +import { SearchResultSet } from '@backstage/search-common'; + +// @public (undocumented) +export class IndexBuilder { + constructor({ logger, searchEngine }: IndexBuilderOptions); + addCollator({ collator, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void; + addDecorator({ decorator }: RegisterDecoratorParameters): void; + build(): Promise<{ + scheduler: Scheduler; + }>; + // (undocumented) + getSearchEngine(): SearchEngine; + } + +// @public (undocumented) +export class LunrSearchEngine implements SearchEngine { + constructor({ logger }: { + logger: Logger; + }); + // (undocumented) + protected docStore: Record; + // (undocumented) + index(type: string, documents: IndexableDocument[]): void; + // (undocumented) + protected logger: Logger; + // (undocumented) + protected lunrIndices: Record; + // (undocumented) + query(query: SearchQuery): Promise; + // (undocumented) + setTranslator(translator: LunrQueryTranslator): void; + // (undocumented) + protected translator: QueryTranslator; +} + +// @public +export class Scheduler { + constructor({ logger }: { + logger: Logger; + }); + addToSchedule(task: Function, interval: number): void; + start(): void; + stop(): void; +} + +// @public +export interface SearchEngine { + index(type: string, documents: IndexableDocument[]): void; + query(query: SearchQuery): Promise; + setTranslator(translator: QueryTranslator): void; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/search-backend/api-report.md b/plugins/search-backend/api-report.md new file mode 100644 index 0000000000..9e617b73ed --- /dev/null +++ b/plugins/search-backend/api-report.md @@ -0,0 +1,17 @@ +## API Report File for "@backstage/plugin-search-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import express from 'express'; +import { Logger } from 'winston'; +import { SearchEngine } from '@backstage/plugin-search-backend-node'; + +// @public (undocumented) +export function createRouter({ engine, logger, }: RouterOptions): Promise; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md new file mode 100644 index 0000000000..181f049f63 --- /dev/null +++ b/plugins/search/api-report.md @@ -0,0 +1,102 @@ +## API Report File for "@backstage/plugin-search" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { AsyncState } from 'react-use/lib/useAsync'; +import { BackstagePlugin } from '@backstage/core'; +import { IndexableDocument } from '@backstage/search-common'; +import { JsonObject } from '@backstage/config'; +import { default as React_2 } from 'react'; +import { ReactElement } from 'react'; +import { RouteRef } from '@backstage/core'; +import { SearchQuery } from '@backstage/search-common'; +import { SearchResult as SearchResult_2 } from '@backstage/search-common'; +import { SearchResultSet } from '@backstage/search-common'; + +// @public (undocumented) +export const DefaultResultListItem: ({ result }: { + result: IndexableDocument; +}) => JSX.Element; + +// @public (undocumented) +export const Filters: ({ filters, filterOptions, resetFilters, updateSelected, updateChecked, }: FiltersProps) => JSX.Element; + +// @public (undocumented) +export const FiltersButton: ({ numberOfSelectedFilters, handleToggleFilters, }: FiltersButtonProps) => JSX.Element; + +// @public (undocumented) +export type FiltersState = { + selected: string; + checked: Array; +}; + +// @public (undocumented) +export const Router: () => JSX.Element; + +// @public (undocumented) +export const searchApiRef: ApiRef; + +// @public (undocumented) +export const SearchBar: ({ className, debounceTime }: Props) => JSX.Element; + +// @public @deprecated (undocumented) +export const SearchBarNext: ({ className, debounceTime }: { + className?: string | undefined; + debounceTime?: number | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const SearchContextProvider: ({ initialState, children, }: React_2.PropsWithChildren<{ + initialState?: SettableSearchContext | undefined; +}>) => JSX.Element; + +// @public (undocumented) +export const SearchFilter: { + ({ component: Element, ...props }: Props_2): JSX.Element; + Checkbox(props: Omit & Component): JSX.Element; + Select(props: Omit & Component): JSX.Element; +}; + +// @public @deprecated (undocumented) +export const SearchFilterNext: { + ({ component: Element, ...props }: Props_2): JSX.Element; + Checkbox(props: Omit & Component): JSX.Element; + Select(props: Omit & Component): JSX.Element; +}; + +// @public (undocumented) +export const SearchPage: () => JSX.Element; + +// @public @deprecated (undocumented) +export const SearchPageNext: () => JSX.Element; + +// @public (undocumented) +const searchPlugin: BackstagePlugin<{ + root: RouteRef; + nextRoot: RouteRef; +}, {}>; + +export { searchPlugin as plugin } + +export { searchPlugin } + +// @public (undocumented) +export const SearchResult: ({ children }: { + children: (results: { + results: SearchResult_2[]; + }) => JSX.Element; +}) => JSX.Element; + +// @public (undocumented) +export const SidebarSearch: () => JSX.Element; + +// @public (undocumented) +export const useSearch: () => SearchContextValue; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/sentry/api-report.md b/plugins/sentry/api-report.md new file mode 100644 index 0000000000..ffd3071b92 --- /dev/null +++ b/plugins/sentry/api-report.md @@ -0,0 +1,100 @@ +## API Report File for "@backstage/plugin-sentry" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { DiscoveryApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const EntitySentryCard: () => JSX.Element; + +// @public (undocumented) +export const EntitySentryContent: () => JSX.Element; + +// @public (undocumented) +export class MockSentryApi implements SentryApi { + // (undocumented) + fetchIssues(): Promise; +} + +// @public (undocumented) +export class ProductionSentryApi implements SentryApi { + constructor(discoveryApi: DiscoveryApi, organization: string); + // (undocumented) + fetchIssues(project: string, statsFor: string): Promise; + } + +// @public (undocumented) +export const Router: ({ entity }: { + entity: Entity; +}) => JSX.Element; + +// @public (undocumented) +export interface SentryApi { + // (undocumented) + fetchIssues(project: string, statsFor: string): Promise; +} + +// @public (undocumented) +export const sentryApiRef: ApiRef; + +// @public (undocumented) +export type SentryIssue = { + platform: SentryPlatform; + lastSeen: string; + numComments: number; + userCount: number; + stats: { + '24h'?: EventPoint[]; + '12h'?: EventPoint[]; + }; + culprit: string; + title: string; + id: string; + assignedTo: any; + logger: any; + type: string; + annotations: any[]; + metadata: SentryIssueMetadata; + status: string; + subscriptionDetails: any; + isPublic: boolean; + hasSeen: boolean; + shortId: string; + shareId: string | null; + firstSeen: string; + count: string; + permalink: string; + level: string; + isSubscribed: boolean; + isBookmarked: boolean; + project: SentryProject; + statusDetails: any; +}; + +// @public (undocumented) +export const SentryIssuesWidget: ({ entity, statsFor, variant, }: { + entity: Entity; + statsFor?: "12h" | "24h" | undefined; + variant?: InfoCardVariants | undefined; +}) => JSX.Element; + +// @public (undocumented) +const sentryPlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + +export { sentryPlugin as plugin } + +export { sentryPlugin } + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/shortcuts/api-report.md b/plugins/shortcuts/api-report.md new file mode 100644 index 0000000000..4c0b3132d6 --- /dev/null +++ b/plugins/shortcuts/api-report.md @@ -0,0 +1,56 @@ +## API Report File for "@backstage/plugin-shortcuts" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { Observable } from '@backstage/core'; +import ObservableImpl from 'zen-observable'; +import { StorageApi } from '@backstage/core'; + +// @public +export class LocalStoredShortcuts implements ShortcutApi { + constructor(storageApi: StorageApi); + // (undocumented) + add(shortcut: Omit): Promise; + // (undocumented) + getColor(url: string): string; + // (undocumented) + remove(id: string): Promise; + // (undocumented) + shortcut$(): ObservableImpl; + // (undocumented) + update(shortcut: Shortcut): Promise; +} + +// @public (undocumented) +export type Shortcut = { + id: string; + url: string; + title: string; +}; + +// @public (undocumented) +export interface ShortcutApi { + add(shortcut: Omit): Promise; + getColor(url: string): string; + remove(id: string): Promise; + shortcut$(): Observable; + update(shortcut: Shortcut): Promise; +} + +// @public (undocumented) +export const Shortcuts: () => JSX.Element; + +// @public (undocumented) +export const shortcutsApiRef: ApiRef; + +// @public (undocumented) +export const shortcutsPlugin: BackstagePlugin<{}, {}>; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/sonarqube/api-report.md b/plugins/sonarqube/api-report.md new file mode 100644 index 0000000000..6d4af3d548 --- /dev/null +++ b/plugins/sonarqube/api-report.md @@ -0,0 +1,41 @@ +## API Report File for "@backstage/plugin-sonarqube" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core'; + +// @public (undocumented) +export const EntitySonarQubeCard: ({ variant, duplicationRatings, }: { + entity?: Entity| undefined; + variant?: InfoCardVariants| undefined; + duplicationRatings?: { + greaterThan: number; + rating: "1.0" | "2.0" | "3.0" | "4.0" | "5.0"; + }[] | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const isSonarQubeAvailable: (entity: Entity) => boolean; + +// @public (undocumented) +export const SonarQubeCard: ({ variant, duplicationRatings, }: { + entity?: Entity | undefined; + variant?: InfoCardVariants | undefined; + duplicationRatings?: DuplicationRating[] | undefined; +}) => JSX.Element; + +// @public (undocumented) +const sonarQubePlugin: BackstagePlugin<{}, {}>; + +export { sonarQubePlugin as plugin } + +export { sonarQubePlugin } + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/splunk-on-call/api-report.md b/plugins/splunk-on-call/api-report.md new file mode 100644 index 0000000000..3a2f085b0b --- /dev/null +++ b/plugins/splunk-on-call/api-report.md @@ -0,0 +1,68 @@ +## API Report File for "@backstage/plugin-splunk-on-call" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { ConfigApi } from '@backstage/core'; +import { DiscoveryApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const EntitySplunkOnCallCard: () => JSX.Element; + +// @public (undocumented) +export const isSplunkOnCallAvailable: (entity: Entity) => boolean; + +// @public (undocumented) +export const splunkOnCallApiRef: ApiRef; + +// @public (undocumented) +export class SplunkOnCallClient implements SplunkOnCallApi { + constructor(config: ClientApiConfig); + // (undocumented) + static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi): SplunkOnCallClient; + // (undocumented) + getEscalationPolicies(): Promise; + // (undocumented) + getIncidents(): Promise; + // (undocumented) + getOnCallUsers(): Promise; + // (undocumented) + getTeams(): Promise; + // (undocumented) + getUsers(): Promise; + // (undocumented) + incidentAction({ routingKey, incidentType, incidentId, incidentDisplayName, incidentMessage, incidentStartTime, }: TriggerAlarmRequest): Promise; + } + +// @public (undocumented) +export const SplunkOnCallPage: { + ({ title, subtitle, pageTitle, }: SplunkOnCallPageProps): JSX.Element; + defaultProps: { + title: string; + subtitle: string; + pageTitle: string; + }; +}; + +// @public (undocumented) +const splunkOnCallPlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + +export { splunkOnCallPlugin as plugin } + +export { splunkOnCallPlugin } + +// @public (undocumented) +export class UnauthorizedError extends Error { +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/tech-radar/api-report.md b/plugins/tech-radar/api-report.md new file mode 100644 index 0000000000..20367c5525 --- /dev/null +++ b/plugins/tech-radar/api-report.md @@ -0,0 +1,123 @@ +## API Report File for "@backstage/plugin-tech-radar" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export interface RadarEntry { + // (undocumented) + description?: string; + // (undocumented) + id: string; + // (undocumented) + key: string; + // (undocumented) + quadrant: string; + // (undocumented) + timeline: Array; + // (undocumented) + title: string; + // (undocumented) + url: string; +} + +// @public (undocumented) +export interface RadarEntrySnapshot { + // (undocumented) + date: Date; + // (undocumented) + description?: string; + // (undocumented) + moved?: MovedState; + // (undocumented) + ringId: string; +} + +// @public (undocumented) +export interface RadarQuadrant { + // (undocumented) + id: string; + // (undocumented) + name: string; +} + +// @public +export interface RadarRing { + // (undocumented) + color: string; + // (undocumented) + id: string; + // (undocumented) + name: string; +} + +// @public (undocumented) +export const Router: { + ({ title, subtitle, pageTitle, ...props }: TechRadarPageProps): JSX.Element; + defaultProps: { + title: string; + subtitle: string; + pageTitle: string; + }; +}; + +// @public (undocumented) +export interface TechRadarApi { + // (undocumented) + load: () => Promise; +} + +// @public (undocumented) +export const techRadarApiRef: ApiRef; + +// @public (undocumented) +export const TechRadarComponent: (props: TechRadarComponentProps) => JSX.Element; + +// @public +export interface TechRadarComponentProps { + // (undocumented) + height: number; + // (undocumented) + svgProps?: object; + // (undocumented) + width: number; +} + +// @public +export interface TechRadarLoaderResponse { + // (undocumented) + entries: RadarEntry[]; + // (undocumented) + quadrants: RadarQuadrant[]; + // (undocumented) + rings: RadarRing[]; +} + +// @public (undocumented) +export const TechRadarPage: { + ({ title, subtitle, pageTitle, ...props }: TechRadarPageProps): JSX.Element; + defaultProps: { + title: string; + subtitle: string; + pageTitle: string; + }; +}; + +// @public (undocumented) +const techRadarPlugin: BackstagePlugin<{ + root: RouteRef; +}, {}>; + +export { techRadarPlugin as plugin } + +export { techRadarPlugin } + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md new file mode 100644 index 0000000000..d433d428e0 --- /dev/null +++ b/plugins/techdocs-backend/api-report.md @@ -0,0 +1,24 @@ +## API Report File for "@backstage/plugin-techdocs-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Config } from '@backstage/config'; +import express from 'express'; +import { GeneratorBuilder } from '@backstage/techdocs-common'; +import { Knex } from 'knex'; +import { Logger } from 'winston'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { PreparerBuilder } from '@backstage/techdocs-common'; +import { PublisherBase } from '@backstage/techdocs-common'; + +// @public (undocumented) +export function createRouter({ preparers, generators, publisher, config, logger, discovery, }: RouterOptions): Promise; + + +export * from "@backstage/techdocs-common"; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md new file mode 100644 index 0000000000..801eabf368 --- /dev/null +++ b/plugins/techdocs/api-report.md @@ -0,0 +1,146 @@ +## API Report File for "@backstage/plugin-techdocs" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { Config } from '@backstage/config'; +import { CSSProperties } from '@material-ui/styles'; +import { DiscoveryApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { EntityName } from '@backstage/catalog-model'; +import { IdentityApi } from '@backstage/core'; +import { Location as Location_2 } from '@backstage/catalog-model'; +import { RouteRef } from '@backstage/core'; + +// @public (undocumented) +export const DocsCardGrid: ({ entities, }: { + entities: Entity[] | undefined; +}) => JSX.Element | null; + +// @public (undocumented) +export const DocsTable: ({ entities, title, }: { + entities: Entity[] | undefined; + title?: string | undefined; +}) => JSX.Element | null; + +// @public (undocumented) +export const EmbeddedDocsRouter: (_props: Props) => JSX.Element; + +// @public (undocumented) +export const EntityTechdocsContent: (_props: { + entity?: Entity| undefined; +}) => JSX.Element; + +// @public (undocumented) +export type PanelType = 'DocsCardGrid' | 'DocsTable'; + +// @public (undocumented) +export const Reader: ({ entityId, onReady }: Props_2) => JSX.Element; + +// @public (undocumented) +export const Router: () => JSX.Element; + +// @public (undocumented) +export interface TechDocsApi { + // (undocumented) + getApiOrigin(): Promise; + // (undocumented) + getEntityMetadata(entityId: EntityName): Promise; + // (undocumented) + getTechDocsMetadata(entityId: EntityName): Promise; +} + +// @public (undocumented) +export const techdocsApiRef: ApiRef; + +// @public +export class TechDocsClient implements TechDocsApi { + constructor({ configApi, discoveryApi, identityApi, }: { + configApi: Config; + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }); + // (undocumented) + configApi: Config; + // (undocumented) + discoveryApi: DiscoveryApi; + // (undocumented) + getApiOrigin(): Promise; + getEntityMetadata(entityId: EntityName): Promise; + getTechDocsMetadata(entityId: EntityName): Promise; + // (undocumented) + identityApi: IdentityApi; +} + +// @public (undocumented) +export const TechDocsCustomHome: ({ tabsConfig, }: { + tabsConfig: TabsConfig; +}) => JSX.Element; + +// @public (undocumented) +export const TechdocsPage: () => JSX.Element; + +// @public (undocumented) +const techdocsPlugin: BackstagePlugin<{ + root: RouteRef; + entityContent: RouteRef; +}, {}>; + +export { techdocsPlugin as plugin } + +export { techdocsPlugin } + +// @public (undocumented) +export const TechDocsReaderPage: () => JSX.Element; + +// @public (undocumented) +export interface TechDocsStorageApi { + // (undocumented) + getApiOrigin(): Promise; + // (undocumented) + getBaseUrl(oldBaseUrl: string, entityId: EntityName, path: string): Promise; + // (undocumented) + getBuilder(): Promise; + // (undocumented) + getEntityDocs(entityId: EntityName, path: string): Promise; + // (undocumented) + getStorageUrl(): Promise; + // (undocumented) + syncEntityDocs(entityId: EntityName): Promise; +} + +// @public (undocumented) +export const techdocsStorageApiRef: ApiRef; + +// @public +export class TechDocsStorageClient implements TechDocsStorageApi { + constructor({ configApi, discoveryApi, identityApi, }: { + configApi: Config; + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }); + // (undocumented) + configApi: Config; + // (undocumented) + discoveryApi: DiscoveryApi; + // (undocumented) + getApiOrigin(): Promise; + // (undocumented) + getBaseUrl(oldBaseUrl: string, entityId: EntityName, path: string): Promise; + // (undocumented) + getBuilder(): Promise; + getEntityDocs(entityId: EntityName, path: string): Promise; + // (undocumented) + getStorageUrl(): Promise; + // (undocumented) + identityApi: IdentityApi; + syncEntityDocs(entityId: EntityName): Promise; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/todo-backend/api-report.md b/plugins/todo-backend/api-report.md new file mode 100644 index 0000000000..8bcfe30aee --- /dev/null +++ b/plugins/todo-backend/api-report.md @@ -0,0 +1,98 @@ +## API Report File for "@backstage/plugin-todo-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CatalogApi } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { EntityName } from '@backstage/catalog-model'; +import express from 'express'; +import { Logger } from 'winston'; +import { ScmIntegrations } from '@backstage/integration'; +import { UrlReader } from '@backstage/backend-common'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export function createTodoParser(options?: TodoParserOptions): TodoParser; + +// @public (undocumented) +export type ListTodosRequest = { + entity?: EntityName; + offset?: number; + limit?: number; + orderBy?: { + field: Fields; + direction: 'asc' | 'desc'; + }; + filters?: { + field: Fields; + value: string; + }[]; +}; + +// @public (undocumented) +export type ListTodosResponse = { + items: TodoItem[]; + totalCount: number; + offset: number; + limit: number; +}; + +// @public (undocumented) +export type ReadTodosOptions = { + url: string; +}; + +// @public (undocumented) +export type ReadTodosResult = { + items: TodoItem[]; +}; + +// @public (undocumented) +export type TodoItem = { + text: string; + tag: string; + author?: string; + viewUrl?: string; + lineNumber?: number; + repoFilePath?: string; +}; + +// @public (undocumented) +export interface TodoReader { + readTodos(options: ReadTodosOptions): Promise; +} + +// @public (undocumented) +export class TodoReaderService implements TodoService { + constructor(options: Options_2); + // (undocumented) + listTodos(req: ListTodosRequest, options?: { + token?: string; + }): Promise; + } + +// @public (undocumented) +export class TodoScmReader implements TodoReader { + constructor(options: Options); + // (undocumented) + static fromConfig(config: Config, options: Omit): TodoScmReader; + // (undocumented) + readTodos({ url }: ReadTodosOptions): Promise; +} + +// @public (undocumented) +export interface TodoService { + // (undocumented) + listTodos(req: ListTodosRequest, options?: { + token?: string; + }): Promise; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/todo/api-report.md b/plugins/todo/api-report.md new file mode 100644 index 0000000000..bdfea8b8f9 --- /dev/null +++ b/plugins/todo/api-report.md @@ -0,0 +1,23 @@ +## API Report File for "@backstage/plugin-todo" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; + +// @public (undocumented) +export const EntityTodoContent: () => JSX.Element; + +// @public (undocumented) +export const todoApiRef: ApiRef; + +// @public (undocumented) +export const todoPlugin: BackstagePlugin<{}, {}>; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/user-settings/api-report.md b/plugins/user-settings/api-report.md new file mode 100644 index 0000000000..2f882f3742 --- /dev/null +++ b/plugins/user-settings/api-report.md @@ -0,0 +1,45 @@ +## API Report File for "@backstage/plugin-user-settings" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApiRef } from '@backstage/core'; +import { BackstagePlugin } from '@backstage/core'; +import { IconComponent } from '@backstage/core'; +import { RouteRef } from '@backstage/core'; +import { SessionApi } from '@backstage/core'; + +// @public (undocumented) +export const AuthProviders: ({ providerSettings }: Props_2) => JSX.Element; + +// @public (undocumented) +export const DefaultProviderSettings: ({ configuredProviders }: Props_3) => JSX.Element; + +// @public (undocumented) +export const ProviderSettingsItem: ({ title, description, icon: Icon, apiRef, }: Props_4) => JSX.Element; + +// @public (undocumented) +export const Router: ({ providerSettings }: Props) => JSX.Element; + +// @public (undocumented) +export const Settings: () => JSX.Element; + +// @public (undocumented) +export const UserSettingsPage: ({ providerSettings }: { + providerSettings?: JSX.Element | undefined; +}) => JSX.Element; + +// @public (undocumented) +const userSettingsPlugin: BackstagePlugin<{ + settingsPage: RouteRef; +}, {}>; + +export { userSettingsPlugin as plugin } + +export { userSettingsPlugin } + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/welcome/api-report.md b/plugins/welcome/api-report.md new file mode 100644 index 0000000000..8d6e1cb156 --- /dev/null +++ b/plugins/welcome/api-report.md @@ -0,0 +1,22 @@ +## API Report File for "@backstage/plugin-welcome" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BackstagePlugin } from '@backstage/core'; + +// @public (undocumented) +export const WelcomePage: () => JSX.Element; + +// @public (undocumented) +const welcomePlugin: BackstagePlugin<{}, {}>; + +export { welcomePlugin as plugin } + +export { welcomePlugin } + + +// (No @packageDocumentation comment for this package) + +``` From f19e0e8e44259d85179ca509e6a34cb90e365d56 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 17 Jun 2021 16:50:54 +0200 Subject: [PATCH 186/206] Change to patch update and clarify changeset message Signed-off-by: Daniel Johansson --- .changeset/good-jars-turn.md | 4 ++-- plugins/search/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/good-jars-turn.md b/.changeset/good-jars-turn.md index 3ba4cb1d36..0bbe344ad2 100644 --- a/.changeset/good-jars-turn.md +++ b/.changeset/good-jars-turn.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-search': minor +'@backstage/plugin-search': patch --- -add IdentityApi support +Use the `identityApi` to forward authorization headers to the `search-backend` diff --git a/plugins/search/package.json b/plugins/search/package.json index dc0041c7a7..6c1f8454d1 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.5.0", + "version": "0.4.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 72663ce3d1b5dbd0dc832a58bd677e005e7ef126 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 17 Jun 2021 17:41:19 +0200 Subject: [PATCH 187/206] revert package.json to HEAD Signed-off-by: Daniel Johansson --- plugins/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/search/package.json b/plugins/search/package.json index 6c1f8454d1..5c84748e06 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.4.1", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 90bd5ab9e7fa086b14a6f5b736fdaea0c5275e55 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 17 Jun 2021 19:19:42 +0200 Subject: [PATCH 188/206] chore: updating the api-docs as some PR's were merged after the api-docs PR merge that were not rebuilt Signed-off-by: blam --- plugins/auth-backend/api-report.md | 21 +++++++++++++++++++++ plugins/explore/api-report.md | 29 ++++++++++++++++++++++++++++- plugins/techdocs/api-report.md | 7 +++++-- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index dea5500d53..4788b9c102 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -6,12 +6,14 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { JSONWebKey } from 'jose'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Profile } from 'passport'; +import { UserEntity } from '@backstage/catalog-model'; // @public (undocumented) export type AuthProviderFactory = (options: AuthProviderFactoryOptions) => AuthProviderRouteHandlers; @@ -47,8 +49,13 @@ export type AuthResponse = { export type BackstageIdentity = { id: string; idToken?: string; + token?: string; + entity?: Entity; }; +// @public (undocumented) +export const createGoogleProvider: (options?: GoogleProviderOptions | undefined) => AuthProviderFactory; + // @public (undocumented) export function createRouter({ logger, config, discovery, database, providerFactories, }: RouterOptions): Promise; @@ -63,6 +70,20 @@ export const encodeState: (state: OAuthState) => string; // @public (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; +// @public (undocumented) +export const googleDefaultSignInResolver: SignInResolver; + +// @public (undocumented) +export const googleEmailSignInResolver: SignInResolver; + +// @public (undocumented) +export type GoogleProviderOptions = { + authHandler?: AuthHandler; + signIn?: { + resolver?: SignInResolver; + }; +}; + // @public export class IdentityClient { constructor(options: { diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index 590af53de8..4116247061 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -5,8 +5,10 @@ ```ts import { BackstagePlugin } from '@backstage/core'; +import { default } from 'react'; import { ExternalRouteRef } from '@backstage/core'; import { RouteRef } from '@backstage/core'; +import { TabProps } from '@material-ui/core'; // @public (undocumented) export const catalogEntityRouteRef: ExternalRouteRef<{ @@ -15,11 +17,22 @@ export const catalogEntityRouteRef: ExternalRouteRef<{ namespace: string; }, false>; +// @public (undocumented) +export const DomainExplorerContent: ({ title, }: { + title?: string | undefined; +}) => JSX.Element; + +// @public +export const ExploreLayout: { + ({ title, subtitle, children, }: ExploreLayoutProps): JSX.Element; + Route: (props: SubRoute) => null; +}; + // @public (undocumented) export const ExplorePage: () => JSX.Element; // @public (undocumented) -export const explorePlugin: BackstagePlugin<{ +const explorePlugin: BackstagePlugin<{ explore: RouteRef; }, { catalogEntity: ExternalRouteRef<{ @@ -29,9 +42,23 @@ export const explorePlugin: BackstagePlugin<{ }, false>; }>; +export { explorePlugin } + +export { explorePlugin as plugin } + // @public (undocumented) export const exploreRouteRef: RouteRef; +// @public (undocumented) +export const GroupsExplorerContent: ({ title, }: { + title?: string | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const ToolExplorerContent: ({ title }: { + title?: string | undefined; +}) => JSX.Element; + // (No @packageDocumentation comment for this package) diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 801eabf368..69a62b4362 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -43,6 +43,9 @@ export const Reader: ({ entityId, onReady }: Props_2) => JSX.Element; // @public (undocumented) export const Router: () => JSX.Element; +// @public (undocumented) +export type SyncResult = 'cached' | 'updated' | 'timeout'; + // @public (undocumented) export interface TechDocsApi { // (undocumented) @@ -109,7 +112,7 @@ export interface TechDocsStorageApi { // (undocumented) getStorageUrl(): Promise; // (undocumented) - syncEntityDocs(entityId: EntityName): Promise; + syncEntityDocs(entityId: EntityName): Promise; } // @public (undocumented) @@ -137,7 +140,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { getStorageUrl(): Promise; // (undocumented) identityApi: IdentityApi; - syncEntityDocs(entityId: EntityName): Promise; + syncEntityDocs(entityId: EntityName): Promise; } From 2d881016cc8f4262243851ced4646fb0dc3800aa Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 18 Jun 2021 10:03:01 +0200 Subject: [PATCH 189/206] chore: dont export default auth provider Signed-off-by: blam Signed-off-by: blam --- plugins/auth-backend/src/providers/google/index.ts | 6 +----- plugins/auth-backend/src/providers/google/provider.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index 8bff8b250b..61805b2dcd 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -14,9 +14,5 @@ * limitations under the License. */ -export { - createGoogleProvider, - googleDefaultSignInResolver, - googleEmailSignInResolver, -} from './provider'; +export { createGoogleProvider, googleEmailSignInResolver } from './provider'; export type { GoogleProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index ace0c8e132..b47399e1c6 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -198,7 +198,7 @@ export const googleEmailSignInResolver: SignInResolver = async ( return { id: entity.metadata.name, entity, token }; }; -export const googleDefaultSignInResolver: SignInResolver = async ( +const googleDefaultSignInResolver: SignInResolver = async ( info, ctx, ) => { From 12942d16c48469a1511f6c0596c9f4e228949a74 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 18 Jun 2021 10:06:27 +0200 Subject: [PATCH 190/206] chore: updating api-report Signed-off-by: blam --- plugins/auth-backend/api-report.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 4788b9c102..6ae878accf 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -70,9 +70,6 @@ export const encodeState: (state: OAuthState) => string; // @public (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; -// @public (undocumented) -export const googleDefaultSignInResolver: SignInResolver; - // @public (undocumented) export const googleEmailSignInResolver: SignInResolver; From 36e9a40849457a012067ab17f8da7f8bbda83d91 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 18 Jun 2021 10:09:35 +0200 Subject: [PATCH 191/206] chore: removing export Signed-off-by: blam --- .changeset/perfect-gifts-compete.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-gifts-compete.md diff --git a/.changeset/perfect-gifts-compete.md b/.changeset/perfect-gifts-compete.md new file mode 100644 index 0000000000..7c03798888 --- /dev/null +++ b/.changeset/perfect-gifts-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Don't export the `defaultGoogleAuthProvider` From 127048f92be4611747a6ac83a46dc7bcd8af0d2e Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 18:38:45 +0200 Subject: [PATCH 192/206] Split `MicrosoftGraphOrgReaderProcessor` into a separate package and make it customizable Signed-off-by: Oliver Sand --- .changeset/metal-badgers-carry.md | 17 + .changeset/silent-ways-laugh.md | 6 + packages/backend/package.json | 2 + .../.eslintrc.js | 3 + .../README.md | 86 ++++ .../config.d.ts | 80 ++++ .../package.json | 53 +++ .../src/index.ts | 18 + .../src/microsoftGraph/client.test.ts | 363 +++++++++++++++ .../src/microsoftGraph/client.ts | 235 ++++++++++ .../src/microsoftGraph/config.test.ts | 75 ++++ .../src/microsoftGraph/config.ts | 91 ++++ .../src/microsoftGraph/constants.ts | 32 ++ .../src/microsoftGraph/helper.test.ts | 29 ++ .../src/microsoftGraph/helper.ts | 22 + .../src/microsoftGraph/index.ts | 35 ++ .../src/microsoftGraph/org.test.ts | 94 ++++ .../src/microsoftGraph/org.ts | 87 ++++ .../src/microsoftGraph/read.test.ts | 354 +++++++++++++++ .../src/microsoftGraph/read.ts | 419 ++++++++++++++++++ .../src/microsoftGraph/types.ts | 32 ++ .../MicrosoftGraphOrgReaderProcessor.ts | 111 +++++ .../src/processors/index.ts | 17 + .../src/setupTests.ts | 17 + .../MicrosoftGraphOrgReaderProcessor.ts | 12 +- yarn.lock | 29 +- 26 files changed, 2312 insertions(+), 7 deletions(-) create mode 100644 .changeset/metal-badgers-carry.md create mode 100644 .changeset/silent-ways-laugh.md create mode 100644 plugins/catalog-backend-extension-msgraph/.eslintrc.js create mode 100644 plugins/catalog-backend-extension-msgraph/README.md create mode 100644 plugins/catalog-backend-extension-msgraph/config.d.ts create mode 100644 plugins/catalog-backend-extension-msgraph/package.json create mode 100644 plugins/catalog-backend-extension-msgraph/src/index.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.test.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.test.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/constants.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.test.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/index.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.test.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/microsoftGraph/types.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/processors/index.ts create mode 100644 plugins/catalog-backend-extension-msgraph/src/setupTests.ts diff --git a/.changeset/metal-badgers-carry.md b/.changeset/metal-badgers-carry.md new file mode 100644 index 0000000000..17592e062f --- /dev/null +++ b/.changeset/metal-badgers-carry.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-extension-msgraph': patch +--- + +Move `MicrosoftGraphOrgReaderProcessor` from `@backstage/plugin-catalog-backend` +to `@backstage/plugin-catalog-backend-extension-msgraph`. + +For now `MicrosoftGraphOrgReaderProcessor` is only deprecated in +`@backstage/plugin-catalog-backend`, but will be removed in the future. While it +is now registered by default, it has to be registered manually in the future. + +TODO: Do we really want to deprecate the transformer before removing it? +It is actually pretty hard to switch to the new transformer as one has to call +`builder.replaceProcessors()` to replace ALL transformers. +As an alternative we can do a breaking change directly with the migration steps +(adding the dependency, adding an import and calling `builder.addProcessor()`). diff --git a/.changeset/silent-ways-laugh.md b/.changeset/silent-ways-laugh.md new file mode 100644 index 0000000000..cdb28a520c --- /dev/null +++ b/.changeset/silent-ways-laugh.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend-extension-msgraph': patch +--- + +Allow customizations of `MicrosoftGraphOrgReaderProcessor` by passing an +optional `groupTransformer`, `userTransformer`, and `organizationTransformer`. diff --git a/packages/backend/package.json b/packages/backend/package.json index f4f304d714..b0fb1ec9d4 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -31,10 +31,12 @@ "@backstage/catalog-client": "^0.3.13", "@backstage/catalog-model": "^0.8.2", "@backstage/config": "^0.1.5", + "@backstage/integration": "^0.5.6", "@backstage/plugin-app-backend": "^0.3.13", "@backstage/plugin-auth-backend": "^0.3.12", "@backstage/plugin-badges-backend": "^0.1.6", "@backstage/plugin-catalog-backend": "^0.10.2", + "@backstage/plugin-catalog-backend-extension-msgraph": "^0.1.0", "@backstage/plugin-code-coverage-backend": "^0.1.6", "@backstage/plugin-graphql-backend": "^0.1.8", "@backstage/plugin-kubernetes-backend": "^0.3.8", diff --git a/plugins/catalog-backend-extension-msgraph/.eslintrc.js b/plugins/catalog-backend-extension-msgraph/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/catalog-backend-extension-msgraph/README.md b/plugins/catalog-backend-extension-msgraph/README.md new file mode 100644 index 0000000000..8296d277df --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/README.md @@ -0,0 +1,86 @@ +# Catalog Backend Extension for Microsoft Graph + +This is an extension to the `plugin-catalog-backend` plugin, providing a +`MicrosoftGraphOrgReaderProcessor` that can be used to ingest organization data +from the Microsoft Graph API. This processor is useful, if you want to import +users and groups from Office 365. + +## Getting Started + +1. The processor is not installed by default, therefore you have to add a + dependency to `@backstage/plugin-catalog-backend-extension-msgraph` to your + backend package. + +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-catalog-backend-extension-msgraph +``` + +2. The `MicrosoftGraphOrgReaderProcessor` is not registered by default, so you have to register it in the catalog plugin: + +```typescript +// packages/backend/src/plugins/catalog.ts +builder.addProcessor( + MicrosoftGraphOrgReaderProcessor.fromConfig(config, { + logger, + }), +); +``` + +3. Configure the processor: + +```yaml +# app-config.yaml +catalog: + processors: + microsoftGraphOrg: + providers: + - target: https://graph.microsoft.com/v1.0 + authority: https://login.microsoftonline.com + tenantId: ${MICROSOFT_GRAPH_TENANT_ID} + clientId: ${MICROSOFT_GRAPH_CLIENT_ID} + clientSecret: ${MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN} + # Optional filter for user, see Microsoft Graph API for the syntax + userFilter: accountEnabled eq true and userType eq 'member' + # Optional filter for group, see Microsoft Graph API for the syntax + groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') +``` + +## Customize the Processor + +In case you want to customize the ingested entities, the `MicrosoftGraphOrgReaderProcessor` allows to pass transformers for users, groups and the organization. + +1. Create a transformer: + +```ts +export async function myGroupTransformer( + group: MicrosoftGraph.Group, + groupPhoto?: string, +): Promise { + if ( + ((group as unknown) as { + creationOptions: string[]; + }).creationOptions.includes('ProvisionGroupHomepage') + ) { + return undefined; + } + + // Transformations may change namespace, change entity naming pattern, fill + // profile with more or other details... + + // Create the group entity on your own, or wrap the default transformer + return await defaultGroupTransformer(group, groupPhoto); +} +``` + +2. Configure the processor with the transformer: + +```ts +builder.addProcessor( + MicrosoftGraphOrgReaderProcessor.fromConfig(config, { + logger, + groupTransformer: myGroupTransformer, + }), +); +``` diff --git a/plugins/catalog-backend-extension-msgraph/config.d.ts b/plugins/catalog-backend-extension-msgraph/config.d.ts new file mode 100644 index 0000000000..d1b9630477 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/config.d.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. + */ + +export interface Config { + /** + * Configuration options for the catalog plugin. + */ + catalog?: { + /** + * List of processor-specific options and attributes + */ + processors?: { + /** + * MicrosoftGraphOrgReaderProcessor configuration + */ + microsoftGraphOrg?: { + /** + * The configuration parameters for each single Microsoft Graph provider. + */ + providers: Array<{ + /** + * The prefix of the target that this matches on, e.g. + * "https://graph.microsoft.com/v1.0", with no trailing slash. + */ + target: string; + /** + * The auth authority used. + * + * Default value "https://login.microsoftonline.com" + */ + authority?: string; + /** + * The tenant whose org data we are interested in. + */ + tenantId: string; + /** + * The OAuth client ID to use for authenticating requests. + */ + clientId: string; + /** + * The OAuth client secret to use for authenticating requests. + * + * @visibility secret + */ + clientSecret: string; + + // TODO: Consider not making these config options and pass them in the + // constructor instead. They are probably not environment specifc, so + // they could also be configured "in code". + + /** + * The filter to apply to extract users. + * + * E.g. "accountEnabled eq true and userType eq 'member'" + */ + userFilter?: string; + /** + * The filter to apply to extract groups. + * + * E.g. "securityEnabled eq false and mailEnabled eq true" + */ + groupFilter?: string; + }>; + }; + }; + }; +} diff --git a/plugins/catalog-backend-extension-msgraph/package.json b/plugins/catalog-backend-extension-msgraph/package.json new file mode 100644 index 0000000000..9feb8e973f --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-catalog-backend-extension-msgraph", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-extension-msgraph" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@azure/msal-node": "^1.1.0", + "@backstage/catalog-model": "^0.8.2", + "@backstage/config": "^0.1.5", + "@backstage/plugin-catalog-backend": "^0.10.2", + "@microsoft/microsoft-graph-types": "^1.25.0", + "cross-fetch": "^3.0.6", + "lodash": "^4.17.15", + "p-limit": "^3.0.2", + "winston": "^3.2.1", + "qs": "^6.9.4" + }, + "devDependencies": { + "@backstage/cli": "^0.7.0", + "@backstage/test-utils": "^0.1.13", + "@types/lodash": "^4.14.151", + "msw": "^0.21.2" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/catalog-backend-extension-msgraph/src/index.ts b/plugins/catalog-backend-extension-msgraph/src/index.ts new file mode 100644 index 0000000000..83c8c14e37 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/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 './processors'; +export * from './microsoftGraph'; diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.test.ts new file mode 100644 index 0000000000..5ef82432bb --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.test.ts @@ -0,0 +1,363 @@ +/* + * 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 msal from '@azure/msal-node'; +import { msw } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { MicrosoftGraphClient } from './client'; + +describe('MicrosoftGraphClient', () => { + const confidentialClientApplication: jest.Mocked = { + acquireTokenByClientCredential: jest.fn(), + } as any; + let client: MicrosoftGraphClient; + const worker = setupServer(); + + msw.setupDefaultHandlers(worker); + + beforeEach(() => { + confidentialClientApplication.acquireTokenByClientCredential.mockResolvedValue( + { token: 'ACCESS_TOKEN' } as any, + ); + client = new MicrosoftGraphClient( + 'https://example.com', + confidentialClientApplication, + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should perform raw request', async () => { + worker.use( + rest.get('https://other.example.com/', (_, res, ctx) => + res(ctx.status(200), ctx.json({ value: 'example' })), + ), + ); + + const response = await client.requestRaw('https://other.example.com/'); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ value: 'example' }); + expect( + confidentialClientApplication.acquireTokenByClientCredential, + ).toBeCalledTimes(1); + expect( + confidentialClientApplication.acquireTokenByClientCredential, + ).toBeCalledWith({ scopes: ['https://graph.microsoft.com/.default'] }); + }); + + it('should perform simple api request', async () => { + worker.use( + rest.get('https://example.com/users', (_, res, ctx) => + res(ctx.status(200), ctx.json({ value: 'example' })), + ), + ); + + const response = await client.requestApi('users'); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ value: 'example' }); + }); + + it('should perform api request with filter, select and expand', async () => { + worker.use( + rest.get('https://example.com/users', (req, res, ctx) => + res(ctx.status(200), ctx.json({ queryString: req.url.search })), + ), + ); + + const response = await client.requestApi('users', { + filter: 'test eq true', + expand: ['children'], + select: ['id', 'children'], + }); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + queryString: + '?$filter=test%20eq%20true&$select=id,children&$expand=children', + }); + }); + + it('should perform collection request for a single page', async () => { + worker.use( + rest.get('https://example.com/users', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + value: ['first'], + }), + ), + ), + ); + + const values = await collectAsyncIterable( + client.requestCollection('users'), + ); + + expect(values).toEqual(['first']); + }); + + it('should perform collection request for multiple pages', async () => { + worker.use( + rest.get('https://example.com/users', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + value: ['first'], + '@odata.nextLink': 'https://example.com/users2', + }), + ), + ), + ); + worker.use( + rest.get('https://example.com/users2', (_, res, ctx) => + res(ctx.status(200), ctx.json({ value: ['second'] })), + ), + ); + + const values = await collectAsyncIterable( + client.requestCollection('users'), + ); + + expect(values).toEqual(['first', 'second']); + }); + + it('should load user profile', async () => { + worker.use( + rest.get('https://example.com/users/user-id', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + surname: 'Example', + }), + ), + ), + ); + + const userProfile = await client.getUserProfile('user-id'); + + expect(userProfile).toEqual({ surname: 'Example' }); + }); + + it('should throw expection if load user profile fails', async () => { + worker.use( + rest.get('https://example.com/users/user-id', (_, res, ctx) => + res(ctx.status(404)), + ), + ); + + await expect(() => client.getUserProfile('user-id')).rejects.toThrowError(); + }); + + it('should load user profile photo with max size of 120', async () => { + worker.use( + rest.get('https://example.com/users/user-id/photos', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + value: [ + { + height: 120, + id: 120, + }, + { + height: 500, + id: 500, + }, + ], + }), + ), + ), + ); + worker.use( + rest.get( + 'https://example.com/users/user-id/photos/120/*', + (_, res, ctx) => res(ctx.status(200), ctx.text('911')), + ), + ); + + const photo = await client.getUserPhotoWithSizeLimit('user-id', 120); + + expect(photo).toEqual('data:image/jpeg;base64,OTEx'); + }); + + it('should not fail if user has no profile photo', async () => { + worker.use( + rest.get('https://example.com/users/user-id/photos', (_, res, ctx) => + res(ctx.status(404)), + ), + ); + + const photo = await client.getUserPhotoWithSizeLimit('user-id', 120); + + expect(photo).toBeFalsy(); + }); + + it('should load user profile photo', async () => { + worker.use( + rest.get('https://example.com/users/user-id/photo/*', (_, res, ctx) => + res(ctx.status(200), ctx.text('911')), + ), + ); + + const photo = await client.getUserPhoto('user-id'); + + expect(photo).toEqual('data:image/jpeg;base64,OTEx'); + }); + + it('should load user profile photo for size 120', async () => { + worker.use( + rest.get( + 'https://example.com/users/user-id/photos/120/*', + (_, res, ctx) => res(ctx.status(200), ctx.text('911')), + ), + ); + + const photo = await client.getUserPhoto('user-id', '120'); + + expect(photo).toEqual('data:image/jpeg;base64,OTEx'); + }); + + it('should load users', async () => { + worker.use( + rest.get('https://example.com/users', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + value: [{ surname: 'Example' }], + }), + ), + ), + ); + + const values = await collectAsyncIterable(client.getUsers()); + + expect(values).toEqual([{ surname: 'Example' }]); + }); + + it('should load group profile photo with max size of 120', async () => { + worker.use( + rest.get('https://example.com/groups/group-id/photos', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + value: [ + { + height: 120, + id: 120, + }, + ], + }), + ), + ), + ); + worker.use( + rest.get( + 'https://example.com/groups/group-id/photos/120/*', + (_, res, ctx) => res(ctx.status(200), ctx.text('911')), + ), + ); + + const photo = await client.getGroupPhotoWithSizeLimit('group-id', 120); + + expect(photo).toEqual('data:image/jpeg;base64,OTEx'); + }); + + it('should load group profile photo', async () => { + worker.use( + rest.get('https://example.com/groups/group-id/photo/*', (_, res, ctx) => + res(ctx.status(200), ctx.text('911')), + ), + ); + + const photo = await client.getGroupPhoto('group-id'); + + expect(photo).toEqual('data:image/jpeg;base64,OTEx'); + }); + + it('should load groups', async () => { + worker.use( + rest.get('https://example.com/groups', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + value: [{ displayName: 'Example' }], + }), + ), + ), + ); + + const values = await collectAsyncIterable(client.getGroups()); + + expect(values).toEqual([{ displayName: 'Example' }]); + }); + + it('should load group members', async () => { + worker.use( + rest.get('https://example.com/groups/group-id/members', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + value: [ + { '@odata.type': '#microsoft.graph.user' }, + { '@odata.type': '#microsoft.graph.group' }, + ], + }), + ), + ), + ); + + const values = await collectAsyncIterable( + client.getGroupMembers('group-id'), + ); + + expect(values).toEqual([ + { '@odata.type': '#microsoft.graph.user' }, + { '@odata.type': '#microsoft.graph.group' }, + ]); + }); + + it('should load organization', async () => { + worker.use( + rest.get('https://example.com/organization/tentant-id', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + displayName: 'Example', + }), + ), + ), + ); + + const organization = await client.getOrganization('tentant-id'); + + expect(organization).toEqual({ displayName: 'Example' }); + }); +}); + +async function collectAsyncIterable( + iterable: AsyncIterable, +): Promise { + const values = []; + for await (const value of iterable) { + values.push(value); + } + return values; +} diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.ts new file mode 100644 index 0000000000..3dfc58e773 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.ts @@ -0,0 +1,235 @@ +/* + * 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 msal from '@azure/msal-node'; +import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; +import fetch from 'cross-fetch'; +import qs from 'qs'; +import { MicrosoftGraphProviderConfig } from './config'; + +export type ODataQuery = { + filter?: string; + expand?: string[]; + select?: string[]; +}; + +export type GroupMember = + | (MicrosoftGraph.Group & { '@odata.type': '#microsoft.graph.user' }) + | (MicrosoftGraph.User & { '@odata.type': '#microsoft.graph.group' }); + +export class MicrosoftGraphClient { + static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient { + const clientConfig: msal.Configuration = { + auth: { + clientId: config.clientId, + clientSecret: config.clientSecret, + authority: `${config.authority}/${config.tenantId}`, + }, + }; + const pca = new msal.ConfidentialClientApplication(clientConfig); + return new MicrosoftGraphClient(config.target, pca); + } + + constructor( + private readonly baseUrl: string, + private readonly pca: msal.ConfidentialClientApplication, + ) {} + + async *requestCollection( + path: string, + query?: ODataQuery, + ): AsyncIterable { + let response = await this.requestApi(path, query); + + for (;;) { + if (response.status !== 200) { + await this.handleError(path, response); + } + + const result = await response.json(); + const elements: T[] = result.value; + + yield* elements; + + // Follow cursor to the next page if one is available + if (!result['@odata.nextLink']) { + return; + } + + response = await this.requestRaw(result['@odata.nextLink']); + } + } + + async requestApi(path: string, query?: ODataQuery): Promise { + const queryString = qs.stringify( + { + $filter: query?.filter, + $select: query?.select?.join(','), + $expand: query?.expand?.join(','), + }, + { + addQueryPrefix: true, + // Microsoft Graph doesn't like an encoded query string + encode: false, + }, + ); + + return await this.requestRaw(`${this.baseUrl}/${path}${queryString}`); + } + + async requestRaw(url: string): Promise { + // Make sure that we always have a valid access token (might be cached) + const token = await this.pca.acquireTokenByClientCredential({ + scopes: ['https://graph.microsoft.com/.default'], + }); + + if (!token) { + throw new Error('Error while requesting token for Microsoft Graph'); + } + + return await fetch(url, { + headers: { + Authorization: `Bearer ${token.accessToken}`, + }, + }); + } + + async getUserProfile(userId: string): Promise { + const response = await this.requestApi(`users/${userId}`); + + if (response.status !== 200) { + await this.handleError('user profile', response); + } + + return await response.json(); + } + + async getUserPhotoWithSizeLimit( + userId: string, + maxSize: number, + ): Promise { + return await this.getPhotoWithSizeLimit('users', userId, maxSize); + } + + async getUserPhoto( + userId: string, + sizeId?: string, + ): Promise { + return await this.getPhoto('users', userId, sizeId); + } + + async *getUsers(query?: ODataQuery): AsyncIterable { + yield* this.requestCollection(`users`, query); + } + + async getGroupPhotoWithSizeLimit( + groupId: string, + maxSize: number, + ): Promise { + return await this.getPhotoWithSizeLimit('groups', groupId, maxSize); + } + + async getGroupPhoto( + groupId: string, + sizeId?: string, + ): Promise { + return await this.getPhoto('groups', groupId, sizeId); + } + + async *getGroups(query?: ODataQuery): AsyncIterable { + yield* this.requestCollection(`groups`, query); + } + + async *getGroupMembers(groupId: string): AsyncIterable { + yield* this.requestCollection(`groups/${groupId}/members`); + } + + async getOrganization( + tenantId: string, + ): Promise { + const response = await this.requestApi(`organization/${tenantId}`); + + if (response.status !== 200) { + await this.handleError(`organization/${tenantId}`, response); + } + + return await response.json(); + } + + private async getPhotoWithSizeLimit( + entityName: string, + id: string, + maxSize: number, + ): Promise { + const response = await this.requestApi(`${entityName}/${id}/photos`); + + if (response.status === 404) { + return undefined; + } else if (response.status !== 200) { + await this.handleError(`${entityName} photos`, response); + } + + const result = await response.json(); + const photos = result.value as MicrosoftGraph.ProfilePhoto[]; + let selectedPhoto: MicrosoftGraph.ProfilePhoto | undefined = undefined; + + // Find the biggest picture that is smaller than the max size + for (const p of photos) { + if ( + !selectedPhoto || + (p.height! >= selectedPhoto.height! && p.height! <= maxSize) + ) { + selectedPhoto = p; + } + } + + if (!selectedPhoto) { + return undefined; + } + + return await this.getPhoto(entityName, id, selectedPhoto.id!); + } + + private async getPhoto( + entityName: string, + id: string, + sizeId?: string, + ): Promise { + const path = sizeId + ? `${entityName}/${id}/photos/${sizeId}/$value` + : `${entityName}/${id}/photo/$value`; + const response = await this.requestApi(path); + + if (response.status === 404) { + return undefined; + } else if (response.status !== 200) { + await this.handleError('photo', response); + } + + return `data:image/jpeg;base64,${Buffer.from( + await response.arrayBuffer(), + ).toString('base64')}`; + } + + private async handleError(path: string, response: Response): Promise { + const result = await response.json(); + const error = result.error as MicrosoftGraph.PublicError; + + throw new Error( + `Error while reading ${path} from Microsoft Graph: ${error.code} - ${error.message}`, + ); + } +} diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.test.ts new file mode 100644 index 0000000000..4671fd23ae --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.test.ts @@ -0,0 +1,75 @@ +/* + * 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 '@backstage/config'; +import { readMicrosoftGraphConfig } from './config'; + +describe('readMicrosoftGraphConfig', () => { + it('applies all of the defaults', () => { + const config = { + providers: [ + { + target: 'target', + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + ], + }; + const actual = readMicrosoftGraphConfig(new ConfigReader(config)); + const expected = [ + { + target: 'target', + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + authority: 'https://login.microsoftonline.com', + userFilter: undefined, + groupFilter: undefined, + }, + ]; + expect(actual).toEqual(expected); + }); + + it('reads all the values', () => { + const config = { + providers: [ + { + target: 'target', + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + authority: 'https://login.example.com/', + userFilter: 'accountEnabled eq true', + groupFilter: 'securityEnabled eq false', + }, + ], + }; + const actual = readMicrosoftGraphConfig(new ConfigReader(config)); + const expected = [ + { + target: 'target', + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + authority: 'https://login.example.com', + userFilter: 'accountEnabled eq true', + groupFilter: 'securityEnabled eq false', + }, + ]; + expect(actual).toEqual(expected); + }); +}); diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.ts new file mode 100644 index 0000000000..72416a63ee --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.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 { Config } from '@backstage/config'; + +/** + * The configuration parameters for a single Microsoft Graph provider. + */ +export type MicrosoftGraphProviderConfig = { + /** + * The prefix of the target that this matches on, e.g. + * "https://graph.microsoft.com/v1.0", with no trailing slash. + */ + target: string; + /** + * The auth authority used. + * + * E.g. "https://login.microsoftonline.com" + */ + authority?: string; + /** + * The tenant whose org data we are interested in. + */ + tenantId: string; + /** + * The OAuth client ID to use for authenticating requests. + */ + clientId: string; + /** + * The OAuth client secret to use for authenticating requests. + * + * @visibility secret + */ + clientSecret: string; + /** + * The filter to apply to extract users. + * + * E.g. "accountEnabled eq true and userType eq 'member'" + */ + userFilter?: string; + /** + * The filter to apply to extract groups. + * + * E.g. "securityEnabled eq false and mailEnabled eq true" + */ + groupFilter?: string; +}; + +export function readMicrosoftGraphConfig( + config: Config, +): MicrosoftGraphProviderConfig[] { + const providers: MicrosoftGraphProviderConfig[] = []; + const providerConfigs = config.getOptionalConfigArray('providers') ?? []; + + for (const providerConfig of providerConfigs) { + const target = providerConfig.getString('target').replace(/\/+$/, ''); + const authority = + providerConfig.getOptionalString('authority')?.replace(/\/+$/, '') || + 'https://login.microsoftonline.com'; + const tenantId = providerConfig.getString('tenantId'); + const clientId = providerConfig.getString('clientId'); + const clientSecret = providerConfig.getString('clientSecret'); + const userFilter = providerConfig.getOptionalString('userFilter'); + const groupFilter = providerConfig.getOptionalString('groupFilter'); + + providers.push({ + target, + authority, + tenantId, + clientId, + clientSecret, + userFilter, + groupFilter, + }); + } + + return providers; +} diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/constants.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/constants.ts new file mode 100644 index 0000000000..6d34d0c159 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/constants.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. + */ + +/** + * The tenant id used by the Microsoft Graph API + */ +export const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION = + 'graph.microsoft.com/tenant-id'; + +/** + * The group id used by the Microsoft Graph API + */ +export const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION = + 'graph.microsoft.com/group-id'; + +/** + * The user id used by the Microsoft Graph API + */ +export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id'; diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.test.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.test.ts new file mode 100644 index 0000000000..48a07bbce8 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.test.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. + */ + +import { normalizeEntityName } from './helper'; + +describe('normalizeEntityName', () => { + it('should normalize name to valid entity name', () => { + expect(normalizeEntityName('User Name')).toBe('user_name'); + }); + + it('should normalize e-mail to valid entity name', () => { + expect(normalizeEntityName('user.name@example.com')).toBe( + 'user.name_example.com', + ); + }); +}); diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.ts new file mode 100644 index 0000000000..e7a804cbff --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.ts @@ -0,0 +1,22 @@ +/* + * 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 function normalizeEntityName(name: string): string { + return name + .trim() + .toLocaleLowerCase() + .replace(/[^a-zA-Z0-9_\-\.]/g, '_'); +} diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/index.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/index.ts new file mode 100644 index 0000000000..89b1a35a79 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/index.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. + */ +export { MicrosoftGraphClient } from './client'; +export { readMicrosoftGraphConfig } from './config'; +export type { MicrosoftGraphProviderConfig } from './config'; +export { + MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, + MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, + MICROSOFT_GRAPH_USER_ID_ANNOTATION, +} from './constants'; +export { normalizeEntityName } from './helper'; +export { + defaultGroupTransformer, + defaultOrganizationTransformer, + defaultUserTransformer, + readMicrosoftGraphOrg, +} from './read'; +export type { + GroupTransformer, + OrganizationTransformer, + UserTransformer, +} from './types'; diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.test.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.test.ts new file mode 100644 index 0000000000..e264847478 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { buildMemberOf, buildOrgHierarchy } from './org'; + +function g( + name: string, + parent: string | undefined, + children: string[], +): GroupEntity { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name }, + spec: { type: 'team', parent, children }, + }; +} + +describe('buildOrgHierarchy', () => { + it('adds groups to their parent.children', () => { + const a = g('a', undefined, []); + const b = g('b', 'a', []); + const c = g('c', 'b', []); + const d = g('d', 'a', []); + buildOrgHierarchy([a, b, c, d]); + expect(a.spec.children).toEqual(expect.arrayContaining(['b', 'd'])); + expect(b.spec.children).toEqual(expect.arrayContaining(['c'])); + expect(c.spec.children).toEqual([]); + expect(d.spec.children).toEqual([]); + }); + + it('sets parent of groups children', () => { + const a = g('a', undefined, ['b', 'd']); + const b = g('b', undefined, ['c']); + const c = g('c', undefined, []); + const d = g('d', undefined, []); + buildOrgHierarchy([a, b, c, d]); + expect(a.spec.parent).toBeUndefined(); + expect(b.spec.parent).toBe('a'); + expect(c.spec.parent).toBe('b'); + expect(d.spec.parent).toBe('a'); + }); +}); + +describe('buildMemberOf', () => { + it('fills indirect member of groups', () => { + const a = g('a', undefined, []); + const b = g('b', 'a', []); + const c = g('c', 'b', []); + const u: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'n' }, + spec: { profile: {}, memberOf: ['c'] }, + }; + + const groups = [a, b, c]; + buildOrgHierarchy(groups); + buildMemberOf(groups, [u]); + expect(u.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + }); + + it('takes group spec.members into account', () => { + const a = g('a', undefined, []); + const b = g('b', 'a', []); + const c = g('c', 'b', []); + c.spec.members = ['n']; + const u: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'n' }, + spec: { profile: {}, memberOf: [] }, + }; + + const groups = [a, b, c]; + buildOrgHierarchy(groups); + buildMemberOf(groups, [u]); + expect(u.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + }); +}); diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.ts new file mode 100644 index 0000000000..69ccc9fb77 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GroupEntity, UserEntity } from '@backstage/catalog-model'; + +// TODO: Copied from plugin-catalog-backend, but we could also export them from +// there. Or move them to catalog-model. + +export function buildOrgHierarchy(groups: GroupEntity[]) { + const groupsByName = new Map(groups.map(g => [g.metadata.name, g])); + + // + // Make sure that g.parent.children contain g + // + + for (const group of groups) { + const selfName = group.metadata.name; + const parentName = group.spec.parent; + if (parentName) { + const parent = groupsByName.get(parentName); + if (parent && !parent.spec.children.includes(selfName)) { + parent.spec.children.push(selfName); + } + } + } + + // + // Make sure that g.children.parent is g + // + + for (const group of groups) { + const selfName = group.metadata.name; + for (const childName of group.spec.children) { + const child = groupsByName.get(childName); + if (child && !child.spec.parent) { + child.spec.parent = selfName; + } + } + } +} + +// Ensure that users have their transitive group memberships. Requires that +// the groups were previously processed with buildOrgHierarchy() +export function buildMemberOf(groups: GroupEntity[], users: UserEntity[]) { + const groupsByName = new Map(groups.map(g => [g.metadata.name, g])); + + users.forEach(user => { + const transitiveMemberOf = new Set(); + + const todo = [ + ...user.spec.memberOf, + ...groups + .filter(g => g.spec.members?.includes(user.metadata.name)) + .map(g => g.metadata.name), + ]; + + for (;;) { + const current = todo.pop(); + if (!current) { + break; + } + + if (!transitiveMemberOf.has(current)) { + transitiveMemberOf.add(current); + const group = groupsByName.get(current); + if (group?.spec.parent) { + todo.push(group.spec.parent); + } + } + } + + user.spec.memberOf = [...transitiveMemberOf]; + }); +} diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts new file mode 100644 index 0000000000..fe63b838cf --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts @@ -0,0 +1,354 @@ +/* + * 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 { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import merge from 'lodash/merge'; +import { GroupMember, MicrosoftGraphClient } from './client'; +import { + readMicrosoftGraphGroups, + readMicrosoftGraphOrganization, + readMicrosoftGraphUsers, + resolveRelations, +} from './read'; + +function user(data: Partial): UserEntity { + return merge( + {}, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'name' }, + spec: { profile: {}, memberOf: [] }, + } as UserEntity, + data, + ); +} + +function group(data: Partial): GroupEntity { + return merge( + {}, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'name', + }, + spec: { + children: [], + type: 'team', + }, + } as GroupEntity, + data, + ); +} + +describe('read microsoft graph', () => { + const client: jest.Mocked = { + getUsers: jest.fn(), + getGroups: jest.fn(), + getGroupMembers: jest.fn(), + getUserPhotoWithSizeLimit: jest.fn(), + getGroupPhotoWithSizeLimit: jest.fn(), + getOrganization: jest.fn(), + } as any; + + afterEach(() => jest.resetAllMocks()); + + describe('readMicrosoftGraphUsers', () => { + it('should read users', async () => { + async function* getExampleUsers() { + yield { + id: 'userid', + displayName: 'User Name', + mail: 'user.name@example.com', + }; + } + + client.getUsers.mockImplementation(getExampleUsers); + client.getUserPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { users } = await readMicrosoftGraphUsers(client, { + userFilter: 'accountEnabled eq true', + }); + + expect(users).toEqual([ + user({ + metadata: { + annotations: { + 'graph.microsoft.com/user-id': 'userid', + }, + name: 'user.name_example.com', + }, + spec: { + profile: { + displayName: 'User Name', + email: 'user.name@example.com', + picture: 'data:image/jpeg;base64,...', + }, + memberOf: [], + }, + }), + ]); + + expect(client.getUsers).toBeCalledTimes(1); + expect(client.getUsers).toBeCalledWith({ + filter: 'accountEnabled eq true', + }); + expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); + expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); + }); + }); + + describe('readMicrosoftGraphOrganization', () => { + it('should read organization', async () => { + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + + const { rootGroup } = await readMicrosoftGraphOrganization( + client, + 'tenantid', + ); + + expect(rootGroup).toEqual( + group({ + metadata: { + annotations: { + 'graph.microsoft.com/tenant-id': 'tenantid', + }, + name: 'organization_name', + description: 'Organization Name', + }, + spec: { + type: 'root', + profile: { + displayName: 'Organization Name', + }, + children: [], + }, + }), + ); + + expect(client.getOrganization).toBeCalledTimes(1); + expect(client.getOrganization).toBeCalledWith('tenantid'); + }); + + it('should read organization with custom transformer', async () => { + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + + const { rootGroup } = await readMicrosoftGraphOrganization( + client, + 'tenantid', + { transformer: async _ => undefined }, + ); + + expect(rootGroup).toEqual(undefined); + + expect(client.getOrganization).toBeCalledTimes(1); + expect(client.getOrganization).toBeCalledWith('tenantid'); + }); + }); + + describe('readMicrosoftGraphGroups', () => { + it('should read groups', async () => { + async function* getExampleGroups() { + yield { + id: 'groupid', + displayName: 'Group Name', + description: 'Group Description', + mail: 'group@example.com', + }; + } + + async function* getExampleGroupMembers(): AsyncIterable { + yield { + '@odata.type': '#microsoft.graph.group', + id: 'childgroupid', + }; + yield { + '@odata.type': '#microsoft.graph.user', + id: 'userid', + }; + } + + client.getGroups.mockImplementation(getExampleGroups); + client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + client.getGroupPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { + groups, + groupMember, + groupMemberOf, + rootGroup, + } = await readMicrosoftGraphGroups(client, 'tenantid', { + groupFilter: 'securityEnabled eq false', + }); + + const expectedRootGroup = group({ + metadata: { + annotations: { + 'graph.microsoft.com/tenant-id': 'tenantid', + }, + name: 'organization_name', + description: 'Organization Name', + }, + spec: { + type: 'root', + profile: { + displayName: 'Organization Name', + }, + children: [], + }, + }); + expect(groups).toEqual([ + expectedRootGroup, + group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'groupid', + }, + name: 'group_name', + description: 'Group Description', + }, + spec: { + type: 'team', + profile: { + displayName: 'Group Name', + email: 'group@example.com', + // TODO: Loading groups photos doesn't work right now as Microsoft + // Graph doesn't allows this yet + /* picture: 'data:image/jpeg;base64,...',*/ + }, + children: [], + }, + }), + ]); + expect(rootGroup).toEqual(expectedRootGroup); + expect(groupMember.get('groupid')).toEqual(new Set(['childgroupid'])); + expect(groupMemberOf.get('userid')).toEqual(new Set(['groupid'])); + expect(groupMember.get('organization_name')).toEqual(new Set()); + + expect(client.getGroups).toBeCalledTimes(1); + expect(client.getGroups).toBeCalledWith({ + filter: 'securityEnabled eq false', + }); + expect(client.getGroupMembers).toBeCalledTimes(1); + expect(client.getGroupMembers).toBeCalledWith('groupid'); + // TODO: Loading groups photos doesn't work right now as Microsoft Graph + // doesn't allows this yet + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); + }); + }); + + describe('resolveRelations', () => { + it('should resolve relations', async () => { + const rootGroup = group({ + metadata: { + annotations: { + 'graph.microsoft.com/tenant-id': 'tenant-id-root', + }, + name: 'root', + }, + spec: { + type: 'root', + children: [], + }, + }); + const groupA = group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'group-id-a', + }, + name: 'a', + }, + }); + const groupB = group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'group-id-b', + }, + name: 'b', + }, + }); + const groupC = group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'group-id-c', + }, + name: 'c', + }, + }); + const user1 = user({ + metadata: { + annotations: { + 'graph.microsoft.com/user-id': 'user-id-1', + }, + name: 'user1', + }, + }); + const user2 = user({ + metadata: { + annotations: { + 'graph.microsoft.com/user-id': 'user-id-2', + }, + name: 'user2', + }, + }); + const groups = [rootGroup, groupA, groupB, groupC]; + const users = [user1, user2]; + const groupMember = new Map>(); + groupMember.set('group-id-b', new Set(['group-id-c'])); + const groupMemberOf = new Map>(); + groupMemberOf.set('user-id-1', new Set(['group-id-a'])); + groupMemberOf.set('user-id-2', new Set(['group-id-c'])); + + // We have a root groups + // We have three groups: a, b, c. c is child of b + // we have two users: u1, u2. u1 is member of a, u2 is member of c + resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); + + expect(rootGroup.spec.parent).toBeUndefined(); + expect(rootGroup.spec.children).toEqual( + expect.arrayContaining(['a', 'b']), + ); + + expect(groupA.spec.parent).toEqual('root'); + expect(groupA.spec.children).toEqual(expect.arrayContaining([])); + + expect(groupB.spec.parent).toEqual('root'); + expect(groupB.spec.children).toEqual(expect.arrayContaining(['c'])); + + expect(groupC.spec.parent).toEqual('b'); + expect(groupC.spec.children).toEqual(expect.arrayContaining([])); + + expect(user1.spec.memberOf).toEqual(expect.arrayContaining(['a'])); + expect(user2.spec.memberOf).toEqual(expect.arrayContaining(['b', 'c'])); + }); + }); +}); diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts new file mode 100644 index 0000000000..308fbf34fd --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts @@ -0,0 +1,419 @@ +/* + * 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 { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; +import limiterFactory from 'p-limit'; +import { MicrosoftGraphClient } from './client'; +import { + MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, + MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, + MICROSOFT_GRAPH_USER_ID_ANNOTATION, +} from './constants'; +import { normalizeEntityName } from './helper'; +import { buildMemberOf, buildOrgHierarchy } from './org'; +import { + GroupTransformer, + OrganizationTransformer, + UserTransformer, +} from './types'; + +export async function defaultUserTransformer( + user: MicrosoftGraph.User, + userPhoto?: string, +): Promise { + if (!user.id || !user.displayName || !user.mail) { + return undefined; + } + + const name = normalizeEntityName(user.mail); + const entity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name, + annotations: { + [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!, + }, + }, + spec: { + profile: { + displayName: user.displayName!, + email: user.mail!, + + // TODO: Additional fields? + // jobTitle: user.jobTitle || undefined, + // officeLocation: user.officeLocation || undefined, + // mobilePhone: user.mobilePhone || undefined, + }, + memberOf: [], + }, + }; + + if (userPhoto) { + entity.spec.profile!.picture = userPhoto; + } + + return entity; +} + +export async function readMicrosoftGraphUsers( + client: MicrosoftGraphClient, + options?: { userFilter?: string; transformer?: UserTransformer }, +): Promise<{ + users: UserEntity[]; // With all relations empty +}> { + const users: UserEntity[] = []; + const limiter = limiterFactory(10); + + const transformer = options?.transformer ?? defaultUserTransformer; + const promises: Promise[] = []; + + for await (const user of client.getUsers({ + filter: options?.userFilter, + })) { + // Process all users in parallel, otherwise it can take quite some time + promises.push( + limiter(async () => { + const userPhoto = await client.getUserPhotoWithSizeLimit( + user.id!, + // We are limiting the photo size, as users with full resolution photos + // can make the Backstage API slow + 120, + ); + + const entity = await transformer(user, userPhoto); + + if (!entity) { + return; + } + + users.push(entity); + }), + ); + } + + // Wait for all users and photos to be downloaded + await Promise.all(promises); + + return { users }; +} + +export async function defaultOrganizationTransformer( + organization: MicrosoftGraph.Organization, +): Promise { + if (!organization.id || !organization.displayName) { + return undefined; + } + + const name = normalizeEntityName(organization.displayName!); + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: name, + description: organization.displayName!, + annotations: { + [MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]: organization.id!, + }, + }, + spec: { + type: 'root', + profile: { + displayName: organization.displayName!, + }, + children: [], + }, + }; +} + +export async function readMicrosoftGraphOrganization( + client: MicrosoftGraphClient, + tenantId: string, + options?: { transformer?: OrganizationTransformer }, +): Promise<{ + rootGroup?: GroupEntity; // With all relations empty +}> { + // For now we expect a single root organization + const organization = await client.getOrganization(tenantId); + const transformer = options?.transformer ?? defaultOrganizationTransformer; + const rootGroup = await transformer(organization); + + return { rootGroup }; +} + +export async function defaultGroupTransformer( + group: MicrosoftGraph.Group, + groupPhoto?: string, +): Promise { + if (!group.id || !group.displayName) { + return undefined; + } + + const name = normalizeEntityName(group.mailNickname || group.displayName); + const entity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: name, + annotations: { + [MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id, + }, + }, + spec: { + type: 'team', + profile: {}, + children: [], + }, + }; + + if (group.description) { + entity.metadata.description = group.description; + } + if (group.displayName) { + entity.spec.profile!.displayName = group.displayName; + } + if (group.mail) { + entity.spec.profile!.email = group.mail; + } + if (groupPhoto) { + entity.spec.profile!.picture = groupPhoto; + } + + return entity; +} + +export async function readMicrosoftGraphGroups( + client: MicrosoftGraphClient, + tenantId: string, + options?: { groupFilter?: string; transformer?: GroupTransformer }, +): Promise<{ + groups: GroupEntity[]; // With all relations empty + rootGroup: GroupEntity | undefined; // With all relations empty + groupMember: Map>; + groupMemberOf: Map>; +}> { + const groups: GroupEntity[] = []; + const groupMember: Map> = new Map(); + const groupMemberOf: Map> = new Map(); + const limiter = limiterFactory(10); + + const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId); + if (rootGroup) { + groupMember.set(rootGroup.metadata.name, new Set()); + groups.push(rootGroup); + } + + const transformer = options?.transformer ?? defaultGroupTransformer; + const promises: Promise[] = []; + + for await (const group of client.getGroups({ + filter: options?.groupFilter, + })) { + // Process all groups in parallel, otherwise it can take quite some time + promises.push( + limiter(async () => { + // TODO: Loading groups photos doesn't work right now as Microsoft Graph + // doesn't allows this yet: https://microsoftgraph.uservoice.com/forums/920506-microsoft-graph-feature-requests/suggestions/37884922-allow-application-to-set-or-update-a-group-s-photo + /* const groupPhoto = await client.getGroupPhotoWithSizeLimit( + group.id!, + // We are limiting the photo size, as groups with full resolution photos + // can make the Backstage API slow + 120, + );*/ + + const entity = await transformer(group /* , groupPhoto*/); + + if (!entity) { + return; + } + + for await (const member of client.getGroupMembers(group.id!)) { + if (!member.id) { + continue; + } + + if (member['@odata.type'] === '#microsoft.graph.user') { + ensureItem(groupMemberOf, member.id, group.id!); + } + + if (member['@odata.type'] === '#microsoft.graph.group') { + ensureItem(groupMember, group.id!, member.id); + } + } + + groups.push(entity); + }), + ); + } + + // Wait for all group members and photos to be loaded + await Promise.all(promises); + + return { + groups, + rootGroup, + groupMember, + groupMemberOf, + }; +} + +export function resolveRelations( + rootGroup: GroupEntity | undefined, + groups: GroupEntity[], + users: UserEntity[], + groupMember: Map>, + groupMemberOf: Map>, +) { + // Build reference lookup tables, we reference them by the id the the graph + const groupMap: Map = new Map(); // by group-id or tenant-id + + for (const group of groups) { + if (group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]) { + groupMap.set( + group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION], + group, + ); + } + if (group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]) { + groupMap.set( + group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION], + group, + ); + } + } + + // Resolve all member relationships into the reverse direction + const parentGroups = new Map>(); + + groupMember.forEach((members, groupId) => + members.forEach(m => ensureItem(parentGroups, m, groupId)), + ); + + // Make sure every group (except root) has at least one parent. If the parent is missing, add the root. + if (rootGroup) { + const tenantId = rootGroup.metadata.annotations![ + MICROSOFT_GRAPH_TENANT_ID_ANNOTATION + ]; + + groups.forEach(group => { + const groupId = group.metadata.annotations![ + MICROSOFT_GRAPH_GROUP_ID_ANNOTATION + ]; + + if (!groupId) { + return; + } + + if (retrieveItems(parentGroups, groupId).size === 0) { + ensureItem(parentGroups, groupId, tenantId); + ensureItem(groupMember, tenantId, groupId); + } + }); + } + + groups.forEach(group => { + const id = + group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] ?? + group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]; + + retrieveItems(groupMember, id).forEach(m => { + const childGroup = groupMap.get(m); + if (childGroup) { + // TODO: This break when groups are transformed into different namespaces, use full entity refs instead + + group.spec.children.push(childGroup.metadata.name); + } + }); + + retrieveItems(parentGroups, id).forEach(p => { + const parentGroup = groupMap.get(p); + if (parentGroup) { + // TODO: Only having a single parent group might not match every companies model, but fine for now. + + // TODO: use full entity refs + group.spec.parent = parentGroup.metadata.name; + } + }); + }); + + // Make sure that all groups have proper parents and children + buildOrgHierarchy(groups); + + // Set relations for all users + users.forEach(user => { + const id = user.metadata.annotations![MICROSOFT_GRAPH_USER_ID_ANNOTATION]; + + retrieveItems(groupMemberOf, id).forEach(p => { + const parentGroup = groupMap.get(p); + if (parentGroup) { + // TODO: use full entity refs + user.spec.memberOf.push(parentGroup.metadata.name); + } + }); + }); + + // Make sure all transitive memberships are available + buildMemberOf(groups, users); +} + +export async function readMicrosoftGraphOrg( + client: MicrosoftGraphClient, + tenantId: string, + options?: { + userFilter?: string; + groupFilter?: string; + groupTransformer?: GroupTransformer; + }, +): Promise<{ users: UserEntity[]; groups: GroupEntity[] }> { + const { users } = await readMicrosoftGraphUsers(client, { + userFilter: options?.userFilter, + }); + const { + groups, + rootGroup, + groupMember, + groupMemberOf, + } = await readMicrosoftGraphGroups(client, tenantId, { + groupFilter: options?.groupFilter, + transformer: options?.groupTransformer, + }); + + resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); + users.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); + groups.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); + + return { users, groups }; +} + +function ensureItem( + target: Map>, + key: string, + value: string, +) { + let set = target.get(key); + if (!set) { + set = new Set(); + target.set(key, set); + } + set!.add(value); +} + +function retrieveItems( + target: Map>, + key: string, +): Set { + return target.get(key) ?? new Set(); +} diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/types.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/types.ts new file mode 100644 index 0000000000..55c28b8d7a --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/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 { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; + +export type UserTransformer = ( + user: MicrosoftGraph.User, + userPhoto?: string, +) => Promise; + +export type OrganizationTransformer = ( + organization: MicrosoftGraph.Organization, +) => Promise; + +export type GroupTransformer = ( + group: MicrosoftGraph.Group, + groupPhoto?: string, +) => Promise; diff --git a/plugins/catalog-backend-extension-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-extension-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts new file mode 100644 index 0000000000..5d8b071663 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -0,0 +1,111 @@ +/* + * 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 { LocationSpec } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + CatalogProcessor, + CatalogProcessorEmit, + results, +} from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; +import { + GroupTransformer, + MicrosoftGraphClient, + MicrosoftGraphProviderConfig, + readMicrosoftGraphConfig, + readMicrosoftGraphOrg, +} from '../microsoftGraph'; + +/** + * Extracts teams and users out of a the Microsoft Graph API. + */ +export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { + private readonly providers: MicrosoftGraphProviderConfig[]; + private readonly logger: Logger; + private readonly groupTransformer?: GroupTransformer; + + static fromConfig( + config: Config, + options: { logger: Logger; groupTransformer?: GroupTransformer }, + ) { + const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg'); + return new MicrosoftGraphOrgReaderProcessor({ + ...options, + providers: c ? readMicrosoftGraphConfig(c) : [], + }); + } + + constructor(options: { + providers: MicrosoftGraphProviderConfig[]; + logger: Logger; + groupTransformer?: GroupTransformer; + }) { + this.providers = options.providers; + this.logger = options.logger; + this.groupTransformer = options.groupTransformer; + } + + async readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise { + if (location.type !== 'microsoft-graph-org') { + return false; + } + + const provider = this.providers.find(p => + location.target.startsWith(p.target), + ); + if (!provider) { + throw new Error( + `There is no Microsoft Graph Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.microsoftGraphOrg.providers.`, + ); + } + + // Read out all of the raw data + const startTimestamp = Date.now(); + this.logger.info('Reading Microsoft Graph users and groups'); + + // We create a client each time as we need one that matches the specific provider + const client = MicrosoftGraphClient.create(provider); + const { users, groups } = await readMicrosoftGraphOrg( + client, + provider.tenantId, + { + userFilter: provider.userFilter, + groupFilter: provider.groupFilter, + groupTransformer: this.groupTransformer, + }, + ); + + const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); + this.logger.debug( + `Read ${users.length} users and ${groups.length} groups from Microsoft Graph in ${duration} seconds`, + ); + + // Done! + for (const group of groups) { + emit(results.entity(location, group)); + } + for (const user of users) { + emit(results.entity(location, user)); + } + + return true; + } +} diff --git a/plugins/catalog-backend-extension-msgraph/src/processors/index.ts b/plugins/catalog-backend-extension-msgraph/src/processors/index.ts new file mode 100644 index 0000000000..46a0cce6f5 --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/src/processors/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 { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor'; diff --git a/plugins/catalog-backend-extension-msgraph/src/setupTests.ts b/plugins/catalog-backend-extension-msgraph/src/setupTests.ts new file mode 100644 index 0000000000..ba33cf996b --- /dev/null +++ b/plugins/catalog-backend-extension-msgraph/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/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts index 3456f2cafa..a25b3bc506 100644 --- a/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -26,8 +26,14 @@ import { import * as results from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; +// TODO: Remove this deprecated processor, the related code in +// ./microsoftGraph/, and the config section in the future. + /** - * Extracts teams and users out of an LDAP server. + * Extracts teams and users out of a the Microsoft Graph API. + * + * @deprecated Use the MicrosoftGraphOrgReaderProcessor from package + * @backstage/plugin-catalog-backend-extension-msgraph instead. */ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { private readonly providers: MicrosoftGraphProviderConfig[]; @@ -58,6 +64,10 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { return false; } + this.logger.warn( + 'MicrosoftGraphOrgReaderProcessor from @backstage/plugin-catalog is deprecated and will be removed in the future. Please migrate to the new one from @backstage/plugin-catalog-backend-extension-msgraph instead.', + ); + const provider = this.providers.find(p => location.target.startsWith(p.target), ); diff --git a/yarn.lock b/yarn.lock index 208ad9e952..ab538fb58a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -218,6 +218,13 @@ dependencies: debug "^4.1.1" +"@azure/msal-common@^4.3.0": + version "4.3.0" + resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.3.0.tgz#b540e92748656724088bf77192e59943a93135bc" + integrity sha512-jFqUWe83wVb6O8cNGGBFg2QlKvqM1ezUgJTEV7kIsAPX0RXhGFE4B1DLNt6hCnkTXDbw+KGW0zgxOEr4MJQwLw== + dependencies: + debug "^4.1.1" + "@azure/msal-node@1.0.0-beta.3", "@azure/msal-node@^1.0.0-beta.3": version "1.0.0-beta.3" resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.3.tgz#c84c7948028b39e48b901f5fac35bdedcbc8772e" @@ -228,6 +235,16 @@ jsonwebtoken "^8.5.1" uuid "^8.3.0" +"@azure/msal-node@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.1.0.tgz#e472cfadead169f8832066ae6c2d6b8eef4e89e4" + integrity sha512-gMO9aZdWOzufp1PcdD5ID25DdS9eInxgeCqx4Tk8PVU6Z7RxJQhoMzS64cJhGdpYgeIQwKljtF0CLCcPFxew/w== + dependencies: + "@azure/msal-common" "^4.3.0" + axios "^0.21.1" + jsonwebtoken "^8.5.1" + uuid "^8.3.0" + "@azure/storage-blob@^12.4.0": version "12.4.0" resolved "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.4.0.tgz#7127ddd9f413105e2c3688691bc4c6245d0806b3" @@ -1347,7 +1364,7 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.7.4": - version "0.8.2" + version "0.8.3" dependencies: "@backstage/config" "^0.1.5" "@backstage/errors" "^0.1.1" @@ -1360,7 +1377,7 @@ yup "^0.29.3" "@backstage/catalog-model@^0.7.9": - version "0.8.2" + version "0.8.3" dependencies: "@backstage/config" "^0.1.5" "@backstage/errors" "^0.1.1" @@ -1389,16 +1406,16 @@ react-use "^17.2.4" "@backstage/plugin-catalog@^0.5.1": - version "0.6.2" + version "0.6.3" dependencies: "@backstage/catalog-client" "^0.3.13" - "@backstage/catalog-model" "^0.8.2" - "@backstage/core" "^0.7.12" + "@backstage/catalog-model" "^0.8.3" + "@backstage/core" "^0.7.13" "@backstage/core-plugin-api" "^0.1.2" "@backstage/errors" "^0.1.1" "@backstage/integration" "^0.5.6" "@backstage/integration-react" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.2.2" + "@backstage/plugin-catalog-react" "^0.2.3" "@backstage/theme" "^0.2.8" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" From 65f5f00c62d786f89e6495c9d662b4513c2370fb Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 31 May 2021 09:41:17 +0200 Subject: [PATCH 193/206] Resolve todos Signed-off-by: Oliver Sand --- .../src/microsoftGraph/read.test.ts | 20 ++++++++++++------- .../src/microsoftGraph/read.ts | 17 ++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts index fe63b838cf..8a5716f277 100644 --- a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts @@ -335,20 +335,26 @@ describe('read microsoft graph', () => { expect(rootGroup.spec.parent).toBeUndefined(); expect(rootGroup.spec.children).toEqual( - expect.arrayContaining(['a', 'b']), + expect.arrayContaining(['group:default/a', 'group:default/b']), ); - expect(groupA.spec.parent).toEqual('root'); + expect(groupA.spec.parent).toEqual('group:default/root'); expect(groupA.spec.children).toEqual(expect.arrayContaining([])); - expect(groupB.spec.parent).toEqual('root'); - expect(groupB.spec.children).toEqual(expect.arrayContaining(['c'])); + expect(groupB.spec.parent).toEqual('group:default/root'); + expect(groupB.spec.children).toEqual( + expect.arrayContaining(['group:default/c']), + ); - expect(groupC.spec.parent).toEqual('b'); + expect(groupC.spec.parent).toEqual('group:default/b'); expect(groupC.spec.children).toEqual(expect.arrayContaining([])); - expect(user1.spec.memberOf).toEqual(expect.arrayContaining(['a'])); - expect(user2.spec.memberOf).toEqual(expect.arrayContaining(['b', 'c'])); + expect(user1.spec.memberOf).toEqual( + expect.arrayContaining(['group:default/a']), + ); + expect(user2.spec.memberOf).toEqual( + expect.arrayContaining(['group:default/c']), + ); }); }); }); diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts index 308fbf34fd..b047a57836 100644 --- a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { + GroupEntity, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import limiterFactory from 'p-limit'; import { MicrosoftGraphClient } from './client'; @@ -332,9 +336,7 @@ export function resolveRelations( retrieveItems(groupMember, id).forEach(m => { const childGroup = groupMap.get(m); if (childGroup) { - // TODO: This break when groups are transformed into different namespaces, use full entity refs instead - - group.spec.children.push(childGroup.metadata.name); + group.spec.children.push(stringifyEntityRef(childGroup)); } }); @@ -342,9 +344,7 @@ export function resolveRelations( const parentGroup = groupMap.get(p); if (parentGroup) { // TODO: Only having a single parent group might not match every companies model, but fine for now. - - // TODO: use full entity refs - group.spec.parent = parentGroup.metadata.name; + group.spec.parent = stringifyEntityRef(parentGroup); } }); }); @@ -359,8 +359,7 @@ export function resolveRelations( retrieveItems(groupMemberOf, id).forEach(p => { const parentGroup = groupMap.get(p); if (parentGroup) { - // TODO: use full entity refs - user.spec.memberOf.push(parentGroup.metadata.name); + user.spec.memberOf.push(stringifyEntityRef(parentGroup)); } }); }); From 8a63e6a523e4f4f2bb436d3aefbdce2d22908a5c Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 10 Jun 2021 12:48:41 +0200 Subject: [PATCH 194/206] Rename from `plugin-catalog-backend-extension-msgraph` to `plugin-catalog-backend-module-msgraph` Signed-off-by: Oliver Sand --- .changeset/metal-badgers-carry.md | 4 ++-- .changeset/silent-ways-laugh.md | 2 +- packages/backend/package.json | 2 +- packages/backend/src/plugins/catalog.ts | 2 +- .../.eslintrc.js | 0 .../README.md | 4 ++-- .../config.d.ts | 0 .../package.json | 4 ++-- .../src/index.ts | 0 .../src/microsoftGraph/client.test.ts | 0 .../src/microsoftGraph/client.ts | 0 .../src/microsoftGraph/config.test.ts | 0 .../src/microsoftGraph/config.ts | 0 .../src/microsoftGraph/constants.ts | 0 .../src/microsoftGraph/helper.test.ts | 0 .../src/microsoftGraph/helper.ts | 0 .../src/microsoftGraph/index.ts | 0 .../src/microsoftGraph/org.test.ts | 0 .../src/microsoftGraph/org.ts | 0 .../src/microsoftGraph/read.test.ts | 0 .../src/microsoftGraph/read.ts | 0 .../src/microsoftGraph/types.ts | 0 .../src/processors/MicrosoftGraphOrgReaderProcessor.ts | 0 .../src/processors/index.ts | 0 .../src/setupTests.ts | 0 .../ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts | 4 ++-- 26 files changed, 11 insertions(+), 11 deletions(-) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/.eslintrc.js (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/README.md (94%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/config.d.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/package.json (90%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/index.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/client.test.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/client.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/config.test.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/config.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/constants.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/helper.test.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/helper.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/index.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/org.test.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/org.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/read.test.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/read.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/microsoftGraph/types.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/processors/MicrosoftGraphOrgReaderProcessor.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/processors/index.ts (100%) rename plugins/{catalog-backend-extension-msgraph => catalog-backend-module-msgraph}/src/setupTests.ts (100%) diff --git a/.changeset/metal-badgers-carry.md b/.changeset/metal-badgers-carry.md index 17592e062f..8d0cc3715d 100644 --- a/.changeset/metal-badgers-carry.md +++ b/.changeset/metal-badgers-carry.md @@ -1,10 +1,10 @@ --- '@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-backend-extension-msgraph': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch --- Move `MicrosoftGraphOrgReaderProcessor` from `@backstage/plugin-catalog-backend` -to `@backstage/plugin-catalog-backend-extension-msgraph`. +to `@backstage/plugin-catalog-backend-module-msgraph`. For now `MicrosoftGraphOrgReaderProcessor` is only deprecated in `@backstage/plugin-catalog-backend`, but will be removed in the future. While it diff --git a/.changeset/silent-ways-laugh.md b/.changeset/silent-ways-laugh.md index cdb28a520c..ea152c68ef 100644 --- a/.changeset/silent-ways-laugh.md +++ b/.changeset/silent-ways-laugh.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend-extension-msgraph': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch --- Allow customizations of `MicrosoftGraphOrgReaderProcessor` by passing an diff --git a/packages/backend/package.json b/packages/backend/package.json index b0fb1ec9d4..aceace8764 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -36,7 +36,7 @@ "@backstage/plugin-auth-backend": "^0.3.12", "@backstage/plugin-badges-backend": "^0.1.6", "@backstage/plugin-catalog-backend": "^0.10.2", - "@backstage/plugin-catalog-backend-extension-msgraph": "^0.1.0", + "@backstage/plugin-catalog-backend-module-msgraph": "^0.1.0", "@backstage/plugin-code-coverage-backend": "^0.1.6", "@backstage/plugin-graphql-backend": "^0.1.8", "@backstage/plugin-kubernetes-backend": "^0.3.8", diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 57afe4fefd..e64fd1f2da 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -16,7 +16,7 @@ import { CatalogBuilder, - createRouter, + createRouter } from '@backstage/plugin-catalog-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; diff --git a/plugins/catalog-backend-extension-msgraph/.eslintrc.js b/plugins/catalog-backend-module-msgraph/.eslintrc.js similarity index 100% rename from plugins/catalog-backend-extension-msgraph/.eslintrc.js rename to plugins/catalog-backend-module-msgraph/.eslintrc.js diff --git a/plugins/catalog-backend-extension-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md similarity index 94% rename from plugins/catalog-backend-extension-msgraph/README.md rename to plugins/catalog-backend-module-msgraph/README.md index 8296d277df..e2f13ebac3 100644 --- a/plugins/catalog-backend-extension-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -8,13 +8,13 @@ users and groups from Office 365. ## Getting Started 1. The processor is not installed by default, therefore you have to add a - dependency to `@backstage/plugin-catalog-backend-extension-msgraph` to your + dependency to `@backstage/plugin-catalog-backend-module-msgraph` to your backend package. ```bash # From your Backstage root directory cd packages/backend -yarn add @backstage/plugin-catalog-backend-extension-msgraph +yarn add @backstage/plugin-catalog-backend-module-msgraph ``` 2. The `MicrosoftGraphOrgReaderProcessor` is not registered by default, so you have to register it in the catalog plugin: diff --git a/plugins/catalog-backend-extension-msgraph/config.d.ts b/plugins/catalog-backend-module-msgraph/config.d.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/config.d.ts rename to plugins/catalog-backend-module-msgraph/config.d.ts diff --git a/plugins/catalog-backend-extension-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json similarity index 90% rename from plugins/catalog-backend-extension-msgraph/package.json rename to plugins/catalog-backend-module-msgraph/package.json index 9feb8e973f..65794cbd91 100644 --- a/plugins/catalog-backend-extension-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/plugin-catalog-backend-extension-msgraph", + "name": "@backstage/plugin-catalog-backend-module-msgraph", "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", @@ -14,7 +14,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "plugins/catalog-backend-extension-msgraph" + "directory": "plugins/catalog-backend-module-msgraph" }, "keywords": [ "backstage" diff --git a/plugins/catalog-backend-extension-msgraph/src/index.ts b/plugins/catalog-backend-module-msgraph/src/index.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/index.ts rename to plugins/catalog-backend-module-msgraph/src/index.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.test.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/client.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.test.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/config.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/constants.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/constants.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/constants.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/constants.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.test.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.test.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.test.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/helper.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/index.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/index.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/org.test.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.test.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/org.test.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/org.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/org.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/org.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.test.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/read.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/microsoftGraph/types.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/microsoftGraph/types.ts rename to plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts rename to plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/processors/index.ts b/plugins/catalog-backend-module-msgraph/src/processors/index.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/processors/index.ts rename to plugins/catalog-backend-module-msgraph/src/processors/index.ts diff --git a/plugins/catalog-backend-extension-msgraph/src/setupTests.ts b/plugins/catalog-backend-module-msgraph/src/setupTests.ts similarity index 100% rename from plugins/catalog-backend-extension-msgraph/src/setupTests.ts rename to plugins/catalog-backend-module-msgraph/src/setupTests.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts index a25b3bc506..40a7251fcf 100644 --- a/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -33,7 +33,7 @@ import { CatalogProcessor, CatalogProcessorEmit } from './types'; * Extracts teams and users out of a the Microsoft Graph API. * * @deprecated Use the MicrosoftGraphOrgReaderProcessor from package - * @backstage/plugin-catalog-backend-extension-msgraph instead. + * @backstage/plugin-catalog-backend-module-msgraph instead. */ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { private readonly providers: MicrosoftGraphProviderConfig[]; @@ -65,7 +65,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { } this.logger.warn( - 'MicrosoftGraphOrgReaderProcessor from @backstage/plugin-catalog is deprecated and will be removed in the future. Please migrate to the new one from @backstage/plugin-catalog-backend-extension-msgraph instead.', + 'MicrosoftGraphOrgReaderProcessor from @backstage/plugin-catalog is deprecated and will be removed in the future. Please migrate to the new one from @backstage/plugin-catalog-backend-module-msgraph instead.', ); const provider = this.providers.find(p => From 265227f32067bf1101d56f777c6606d88422f11a Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 10 Jun 2021 13:00:09 +0200 Subject: [PATCH 195/206] Remove example code Signed-off-by: Oliver Sand --- .changeset/metal-badgers-carry.md | 6 ------ packages/backend/package.json | 1 - 2 files changed, 7 deletions(-) diff --git a/.changeset/metal-badgers-carry.md b/.changeset/metal-badgers-carry.md index 8d0cc3715d..11f1e2f01b 100644 --- a/.changeset/metal-badgers-carry.md +++ b/.changeset/metal-badgers-carry.md @@ -9,9 +9,3 @@ to `@backstage/plugin-catalog-backend-module-msgraph`. For now `MicrosoftGraphOrgReaderProcessor` is only deprecated in `@backstage/plugin-catalog-backend`, but will be removed in the future. While it is now registered by default, it has to be registered manually in the future. - -TODO: Do we really want to deprecate the transformer before removing it? -It is actually pretty hard to switch to the new transformer as one has to call -`builder.replaceProcessors()` to replace ALL transformers. -As an alternative we can do a breaking change directly with the migration steps -(adding the dependency, adding an import and calling `builder.addProcessor()`). diff --git a/packages/backend/package.json b/packages/backend/package.json index aceace8764..5195a7c28f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -36,7 +36,6 @@ "@backstage/plugin-auth-backend": "^0.3.12", "@backstage/plugin-badges-backend": "^0.1.6", "@backstage/plugin-catalog-backend": "^0.10.2", - "@backstage/plugin-catalog-backend-module-msgraph": "^0.1.0", "@backstage/plugin-code-coverage-backend": "^0.1.6", "@backstage/plugin-graphql-backend": "^0.1.8", "@backstage/plugin-kubernetes-backend": "^0.3.8", From 4384bc4ec1839a924a9065f95e6141533a16f3e1 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 15 Jun 2021 17:57:07 +0200 Subject: [PATCH 196/206] Fix formatting Signed-off-by: Oliver Sand --- packages/backend/src/plugins/catalog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index e64fd1f2da..57afe4fefd 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -16,7 +16,7 @@ import { CatalogBuilder, - createRouter + createRouter, } from '@backstage/plugin-catalog-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; From d4d9f13693d6e3a5915ca1be0cf63ae08b5f36cf Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 16 Jun 2021 09:43:40 +0200 Subject: [PATCH 197/206] Improve README Signed-off-by: Oliver Sand --- .../catalog-backend-module-msgraph/README.md | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index e2f13ebac3..0f1fc360e6 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -1,8 +1,8 @@ -# Catalog Backend Extension for Microsoft Graph +# Catalog Backend Module for Microsoft Graph -This is an extension to the `plugin-catalog-backend` plugin, providing a +This is an extension module to the `plugin-catalog-backend` plugin, providing a `MicrosoftGraphOrgReaderProcessor` that can be used to ingest organization data -from the Microsoft Graph API. This processor is useful, if you want to import +from the Microsoft Graph API. This processor is useful if you want to import users and groups from Office 365. ## Getting Started @@ -17,7 +17,8 @@ cd packages/backend yarn add @backstage/plugin-catalog-backend-module-msgraph ``` -2. The `MicrosoftGraphOrgReaderProcessor` is not registered by default, so you have to register it in the catalog plugin: +2. The `MicrosoftGraphOrgReaderProcessor` is not registered by default, so you + have to register it in the catalog plugin: ```typescript // packages/backend/src/plugins/catalog.ts @@ -28,7 +29,13 @@ builder.addProcessor( ); ``` -3. Configure the processor: +3. Create or use an existing App registration in the [Microsoft Azure Portal](https://portal.azure.com/). + The App registration requires at least the API permissions `Group.Read.All`, + `GroupMember.Read.All`, `User.Read` and `User.Read.All` for Microsoft Graph + (if you still run into errors about insufficient privileges, add + `Team.ReadBasic.All` and `TeamMember.Read.All` too). + +4. Configure the processor: ```yaml # app-config.yaml @@ -38,18 +45,41 @@ catalog: providers: - target: https://graph.microsoft.com/v1.0 authority: https://login.microsoftonline.com + # If you don't know you tenantId, you can use Microsoft Graph Explorer + # to query it tenantId: ${MICROSOFT_GRAPH_TENANT_ID} + # Client Id and Secret can be created under Certificates & secrets in + # the App registration in the Microsoft Azure Portal. clientId: ${MICROSOFT_GRAPH_CLIENT_ID} clientSecret: ${MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN} # Optional filter for user, see Microsoft Graph API for the syntax + # See https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties + # and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter userFilter: accountEnabled eq true and userType eq 'member' # Optional filter for group, see Microsoft Graph API for the syntax + # See https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') ``` +5. Add a location that ingests from Microsoft Graph: + +```yaml +# app-config.yaml +catalog: + locations: + - type: microsoft-graph-org + target: https://graph.microsoft.com/v1.0 + # If you catalog doesn't allow to import Group and User entities by + # default, allow them here + rules: + - allow: [Group, User] + … +``` + ## Customize the Processor -In case you want to customize the ingested entities, the `MicrosoftGraphOrgReaderProcessor` allows to pass transformers for users, groups and the organization. +In case you want to customize the ingested entities, the `MicrosoftGraphOrgReaderProcessor` +allows to pass transformers for users, groups and the organization. 1. Create a transformer: From 160181779b919f79f7b73b090bc7a42c9daef17a Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 16 Jun 2021 09:53:28 +0200 Subject: [PATCH 198/206] Add docs to microsite Closes #4627 Signed-off-by: Oliver Sand --- docs/integrations/azure/locations.md | 4 ++-- docs/integrations/azure/org.md | 14 ++++++++++++++ microsite/sidebars.json | 7 +++++-- mkdocs.yml | 3 ++- plugins/catalog-backend-module-msgraph/README.md | 2 +- 5 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 docs/integrations/azure/org.md diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index 163674f00a..3b3e3dd5ed 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -6,8 +6,8 @@ sidebar_label: Locations description: Integrating source code stored in Azure DevOps into the Backstage catalog --- -The Azure integration supports loading catalog entities from Azure DevOps. -Entities can be added to +The Azure DevOps integration supports loading catalog entities from Azure +DevOps. Entities can be added to [static catalog configuration](../../features/software-catalog/configuration.md), or registered with the [catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import) diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md new file mode 100644 index 0000000000..c359960f4d --- /dev/null +++ b/docs/integrations/azure/org.md @@ -0,0 +1,14 @@ +--- +id: org +title: Microsoft Azure Active Directory Organizational Data +sidebar_label: Org Data +# prettier-ignore +description: Importing users and groups from a Microsoft Azure Active Directory into Backstage +--- + +The Backstage catalog can be set up to ingest organizational data - users and +teams - directly from an tenant in Microsoft Azure Active Directory via the +Microsoft Graph API. + +More details on this are available in the +[README of the `@backstage/plugin-catalog-backend-module-msgraph` package](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md). diff --git a/microsite/sidebars.json b/microsite/sidebars.json index e0add7a007..9021d15884 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -102,8 +102,11 @@ "integrations/index", { "type": "subcategory", - "label": "Azure DevOps", - "ids": ["integrations/azure/locations"] + "label": "Azure", + "ids": [ + "integrations/azure/locations", + "integrations/azure/org" + ] }, { "type": "subcategory", diff --git a/mkdocs.yml b/mkdocs.yml index 4b2b29a997..6c4af107ce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,8 +75,9 @@ nav: - FAQ: 'features/techdocs/FAQ.md' - Integrations: - Overview: 'integrations/index.md' - - Azure DevOps: + - Azure: - Locations: 'integrations/azure/locations.md' + - Org Data: 'integrations/azure/org.md' - Bitbucket: - Locations: 'integrations/bitbucket/locations.md' - Discovery: 'integrations/bitbucket/discovery.md' diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index 0f1fc360e6..df141901aa 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -3,7 +3,7 @@ This is an extension module to the `plugin-catalog-backend` plugin, providing a `MicrosoftGraphOrgReaderProcessor` that can be used to ingest organization data from the Microsoft Graph API. This processor is useful if you want to import -users and groups from Office 365. +users and groups from Azure Active Directory or Office 365. ## Getting Started From 579cf97f0219f8e36b760239dffcf97b160d680c Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 16 Jun 2021 11:14:53 +0200 Subject: [PATCH 199/206] Remove `MicrosoftGraphOrgReaderProcessor` from `plugin-catalog-backend` Signed-off-by: Oliver Sand --- .changeset/metal-badgers-carry.md | 20 +- plugins/catalog-backend/config.d.ts | 48 --- plugins/catalog-backend/package.json | 2 - .../MicrosoftGraphOrgReaderProcessor.ts | 110 ------ .../src/ingestion/processors/index.ts | 1 - .../processors/microsoftGraph/client.test.ts | 363 ------------------ .../processors/microsoftGraph/client.ts | 235 ------------ .../processors/microsoftGraph/config.test.ts | 75 ---- .../processors/microsoftGraph/config.ts | 91 ----- .../processors/microsoftGraph/constants.ts | 32 -- .../processors/microsoftGraph/index.ts | 24 -- .../processors/microsoftGraph/read.test.ts | 347 ----------------- .../processors/microsoftGraph/read.ts | 363 ------------------ .../src/next/NextCatalogBuilder.ts | 2 - .../src/service/CatalogBuilder.ts | 2 - yarn.lock | 2 +- 16 files changed, 18 insertions(+), 1699 deletions(-) delete mode 100644 plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts delete mode 100644 plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.test.ts delete mode 100644 plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.ts delete mode 100644 plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.test.ts delete mode 100644 plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.ts delete mode 100644 plugins/catalog-backend/src/ingestion/processors/microsoftGraph/constants.ts delete mode 100644 plugins/catalog-backend/src/ingestion/processors/microsoftGraph/index.ts delete mode 100644 plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.test.ts delete mode 100644 plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.ts diff --git a/.changeset/metal-badgers-carry.md b/.changeset/metal-badgers-carry.md index 11f1e2f01b..b82e638c6f 100644 --- a/.changeset/metal-badgers-carry.md +++ b/.changeset/metal-badgers-carry.md @@ -6,6 +6,20 @@ Move `MicrosoftGraphOrgReaderProcessor` from `@backstage/plugin-catalog-backend` to `@backstage/plugin-catalog-backend-module-msgraph`. -For now `MicrosoftGraphOrgReaderProcessor` is only deprecated in -`@backstage/plugin-catalog-backend`, but will be removed in the future. While it -is now registered by default, it has to be registered manually in the future. +The `MicrosoftGraphOrgReaderProcessor` isn't registered by default anymore, if +you want to continue using it you have to register it manually at the catalog +builder: + +1. Add dependency to `@backstage/plugin-catalog-backend-module-msgraph` to the `package.json` of your backend. +2. Add the processor to the catalog builder: + +```typescript +// packages/backend/src/plugins/catalog.ts +builder.addProcessor( + MicrosoftGraphOrgReaderProcessor.fromConfig(config, { + logger, + }), +); +``` + +For more configuration details, see the [README of the `@backstage/plugin-catalog-backend-module-msgraph` package](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md). diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 20fcd975cf..5417a84741 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -362,54 +362,6 @@ export interface Config { roleArn?: string; }; }; - - /** - * MicrosoftGraphOrgReaderProcessor configuration - */ - microsoftGraphOrg?: { - /** - * The configuration parameters for each single Microsoft Graph provider. - */ - providers: Array<{ - /** - * The prefix of the target that this matches on, e.g. - * "https://graph.microsoft.com/v1.0", with no trailing slash. - */ - target: string; - /** - * The auth authority used. - * - * Default value "https://login.microsoftonline.com" - */ - authority?: string; - /** - * The tenant whose org data we are interested in. - */ - tenantId: string; - /** - * The OAuth client ID to use for authenticating requests. - */ - clientId: string; - /** - * The OAuth client secret to use for authenticating requests. - * - * @visibility secret - */ - clientSecret: string; - /** - * The filter to apply to extract users. - * - * E.g. "accountEnabled eq true and userType eq 'member'" - */ - userFilter?: string; - /** - * The filter to apply to extract groups. - * - * E.g. "securityEnabled eq false and mailEnabled eq true" - */ - groupFilter?: string; - }>; - }; }; }; } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 6bc53b43da..8e9f54504e 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -29,7 +29,6 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@azure/msal-node": "^1.0.0-beta.3", "@backstage/backend-common": "^0.8.3", "@backstage/catalog-client": "^0.3.13", "@backstage/catalog-model": "^0.8.3", @@ -38,7 +37,6 @@ "@backstage/integration": "^0.5.6", "@backstage/plugin-search-backend-node": "^0.2.1", "@backstage/search-common": "^0.1.2", - "@microsoft/microsoft-graph-types": "^1.25.0", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", "@types/ldapjs": "^1.0.9", diff --git a/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts deleted file mode 100644 index 40a7251fcf..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.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. - */ - -import { LocationSpec } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; -import { Logger } from 'winston'; -import { - MicrosoftGraphClient, - MicrosoftGraphProviderConfig, - readMicrosoftGraphConfig, - readMicrosoftGraphOrg, -} from './microsoftGraph'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; - -// TODO: Remove this deprecated processor, the related code in -// ./microsoftGraph/, and the config section in the future. - -/** - * Extracts teams and users out of a the Microsoft Graph API. - * - * @deprecated Use the MicrosoftGraphOrgReaderProcessor from package - * @backstage/plugin-catalog-backend-module-msgraph instead. - */ -export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { - private readonly providers: MicrosoftGraphProviderConfig[]; - private readonly logger: Logger; - - static fromConfig(config: Config, options: { logger: Logger }) { - const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg'); - return new MicrosoftGraphOrgReaderProcessor({ - ...options, - providers: c ? readMicrosoftGraphConfig(c) : [], - }); - } - - constructor(options: { - providers: MicrosoftGraphProviderConfig[]; - logger: Logger; - }) { - this.providers = options.providers; - this.logger = options.logger; - } - - async readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise { - if (location.type !== 'microsoft-graph-org') { - return false; - } - - this.logger.warn( - 'MicrosoftGraphOrgReaderProcessor from @backstage/plugin-catalog is deprecated and will be removed in the future. Please migrate to the new one from @backstage/plugin-catalog-backend-module-msgraph instead.', - ); - - const provider = this.providers.find(p => - location.target.startsWith(p.target), - ); - if (!provider) { - throw new Error( - `There is no Microsoft Graph Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.microsoftGraphOrg.providers.`, - ); - } - - // Read out all of the raw data - const startTimestamp = Date.now(); - this.logger.info('Reading Microsoft Graph users and groups'); - - // We create a client each time as we need one that matches the specific provider - const client = MicrosoftGraphClient.create(provider); - const { users, groups } = await readMicrosoftGraphOrg( - client, - provider.tenantId, - { - userFilter: provider.userFilter, - groupFilter: provider.groupFilter, - }, - ); - - const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); - this.logger.debug( - `Read ${users.length} users and ${groups.length} groups from Microsoft Graph in ${duration} seconds`, - ); - - // Done! - for (const group of groups) { - emit(results.entity(location, group)); - } - for (const user of users) { - emit(results.entity(location, user)); - } - - return true; - } -} diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index a92cc9ed3b..8b87e18018 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -27,7 +27,6 @@ export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; export { LocationEntityProcessor } from './LocationEntityProcessor'; -export { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor'; export { PlaceholderProcessor } from './PlaceholderProcessor'; export type { PlaceholderResolver } from './PlaceholderProcessor'; export { StaticLocationProcessor } from './StaticLocationProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.test.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.test.ts deleted file mode 100644 index 5ef82432bb..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.test.ts +++ /dev/null @@ -1,363 +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 msal from '@azure/msal-node'; -import { msw } from '@backstage/test-utils'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { MicrosoftGraphClient } from './client'; - -describe('MicrosoftGraphClient', () => { - const confidentialClientApplication: jest.Mocked = { - acquireTokenByClientCredential: jest.fn(), - } as any; - let client: MicrosoftGraphClient; - const worker = setupServer(); - - msw.setupDefaultHandlers(worker); - - beforeEach(() => { - confidentialClientApplication.acquireTokenByClientCredential.mockResolvedValue( - { token: 'ACCESS_TOKEN' } as any, - ); - client = new MicrosoftGraphClient( - 'https://example.com', - confidentialClientApplication, - ); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should perform raw request', async () => { - worker.use( - rest.get('https://other.example.com/', (_, res, ctx) => - res(ctx.status(200), ctx.json({ value: 'example' })), - ), - ); - - const response = await client.requestRaw('https://other.example.com/'); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ value: 'example' }); - expect( - confidentialClientApplication.acquireTokenByClientCredential, - ).toBeCalledTimes(1); - expect( - confidentialClientApplication.acquireTokenByClientCredential, - ).toBeCalledWith({ scopes: ['https://graph.microsoft.com/.default'] }); - }); - - it('should perform simple api request', async () => { - worker.use( - rest.get('https://example.com/users', (_, res, ctx) => - res(ctx.status(200), ctx.json({ value: 'example' })), - ), - ); - - const response = await client.requestApi('users'); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ value: 'example' }); - }); - - it('should perform api request with filter, select and expand', async () => { - worker.use( - rest.get('https://example.com/users', (req, res, ctx) => - res(ctx.status(200), ctx.json({ queryString: req.url.search })), - ), - ); - - const response = await client.requestApi('users', { - filter: 'test eq true', - expand: ['children'], - select: ['id', 'children'], - }); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ - queryString: - '?$filter=test%20eq%20true&$select=id,children&$expand=children', - }); - }); - - it('should perform collection request for a single page', async () => { - worker.use( - rest.get('https://example.com/users', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: ['first'], - }), - ), - ), - ); - - const values = await collectAsyncIterable( - client.requestCollection('users'), - ); - - expect(values).toEqual(['first']); - }); - - it('should perform collection request for multiple pages', async () => { - worker.use( - rest.get('https://example.com/users', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: ['first'], - '@odata.nextLink': 'https://example.com/users2', - }), - ), - ), - ); - worker.use( - rest.get('https://example.com/users2', (_, res, ctx) => - res(ctx.status(200), ctx.json({ value: ['second'] })), - ), - ); - - const values = await collectAsyncIterable( - client.requestCollection('users'), - ); - - expect(values).toEqual(['first', 'second']); - }); - - it('should load user profile', async () => { - worker.use( - rest.get('https://example.com/users/user-id', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - surname: 'Example', - }), - ), - ), - ); - - const userProfile = await client.getUserProfile('user-id'); - - expect(userProfile).toEqual({ surname: 'Example' }); - }); - - it('should throw expection if load user profile fails', async () => { - worker.use( - rest.get('https://example.com/users/user-id', (_, res, ctx) => - res(ctx.status(404)), - ), - ); - - await expect(() => client.getUserProfile('user-id')).rejects.toThrowError(); - }); - - it('should load user profile photo with max size of 120', async () => { - worker.use( - rest.get('https://example.com/users/user-id/photos', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: [ - { - height: 120, - id: 120, - }, - { - height: 500, - id: 500, - }, - ], - }), - ), - ), - ); - worker.use( - rest.get( - 'https://example.com/users/user-id/photos/120/*', - (_, res, ctx) => res(ctx.status(200), ctx.text('911')), - ), - ); - - const photo = await client.getUserPhotoWithSizeLimit('user-id', 120); - - expect(photo).toEqual('data:image/jpeg;base64,OTEx'); - }); - - it('should not fail if user has no profile photo', async () => { - worker.use( - rest.get('https://example.com/users/user-id/photos', (_, res, ctx) => - res(ctx.status(404)), - ), - ); - - const photo = await client.getUserPhotoWithSizeLimit('user-id', 120); - - expect(photo).toBeFalsy(); - }); - - it('should load user profile photo', async () => { - worker.use( - rest.get('https://example.com/users/user-id/photo/*', (_, res, ctx) => - res(ctx.status(200), ctx.text('911')), - ), - ); - - const photo = await client.getUserPhoto('user-id'); - - expect(photo).toEqual('data:image/jpeg;base64,OTEx'); - }); - - it('should load user profile photo for size 120', async () => { - worker.use( - rest.get( - 'https://example.com/users/user-id/photos/120/*', - (_, res, ctx) => res(ctx.status(200), ctx.text('911')), - ), - ); - - const photo = await client.getUserPhoto('user-id', '120'); - - expect(photo).toEqual('data:image/jpeg;base64,OTEx'); - }); - - it('should load users', async () => { - worker.use( - rest.get('https://example.com/users', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: [{ surname: 'Example' }], - }), - ), - ), - ); - - const values = await collectAsyncIterable(client.getUsers()); - - expect(values).toEqual([{ surname: 'Example' }]); - }); - - it('should load group profile photo with max size of 120', async () => { - worker.use( - rest.get('https://example.com/groups/group-id/photos', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: [ - { - height: 120, - id: 120, - }, - ], - }), - ), - ), - ); - worker.use( - rest.get( - 'https://example.com/groups/group-id/photos/120/*', - (_, res, ctx) => res(ctx.status(200), ctx.text('911')), - ), - ); - - const photo = await client.getGroupPhotoWithSizeLimit('group-id', 120); - - expect(photo).toEqual('data:image/jpeg;base64,OTEx'); - }); - - it('should load group profile photo', async () => { - worker.use( - rest.get('https://example.com/groups/group-id/photo/*', (_, res, ctx) => - res(ctx.status(200), ctx.text('911')), - ), - ); - - const photo = await client.getGroupPhoto('group-id'); - - expect(photo).toEqual('data:image/jpeg;base64,OTEx'); - }); - - it('should load groups', async () => { - worker.use( - rest.get('https://example.com/groups', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: [{ displayName: 'Example' }], - }), - ), - ), - ); - - const values = await collectAsyncIterable(client.getGroups()); - - expect(values).toEqual([{ displayName: 'Example' }]); - }); - - it('should load group members', async () => { - worker.use( - rest.get('https://example.com/groups/group-id/members', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: [ - { '@odata.type': '#microsoft.graph.user' }, - { '@odata.type': '#microsoft.graph.group' }, - ], - }), - ), - ), - ); - - const values = await collectAsyncIterable( - client.getGroupMembers('group-id'), - ); - - expect(values).toEqual([ - { '@odata.type': '#microsoft.graph.user' }, - { '@odata.type': '#microsoft.graph.group' }, - ]); - }); - - it('should load organization', async () => { - worker.use( - rest.get('https://example.com/organization/tentant-id', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - displayName: 'Example', - }), - ), - ), - ); - - const organization = await client.getOrganization('tentant-id'); - - expect(organization).toEqual({ displayName: 'Example' }); - }); -}); - -async function collectAsyncIterable( - iterable: AsyncIterable, -): Promise { - const values = []; - for await (const value of iterable) { - values.push(value); - } - return values; -} diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.ts deleted file mode 100644 index 3dfc58e773..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.ts +++ /dev/null @@ -1,235 +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 msal from '@azure/msal-node'; -import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; -import fetch from 'cross-fetch'; -import qs from 'qs'; -import { MicrosoftGraphProviderConfig } from './config'; - -export type ODataQuery = { - filter?: string; - expand?: string[]; - select?: string[]; -}; - -export type GroupMember = - | (MicrosoftGraph.Group & { '@odata.type': '#microsoft.graph.user' }) - | (MicrosoftGraph.User & { '@odata.type': '#microsoft.graph.group' }); - -export class MicrosoftGraphClient { - static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient { - const clientConfig: msal.Configuration = { - auth: { - clientId: config.clientId, - clientSecret: config.clientSecret, - authority: `${config.authority}/${config.tenantId}`, - }, - }; - const pca = new msal.ConfidentialClientApplication(clientConfig); - return new MicrosoftGraphClient(config.target, pca); - } - - constructor( - private readonly baseUrl: string, - private readonly pca: msal.ConfidentialClientApplication, - ) {} - - async *requestCollection( - path: string, - query?: ODataQuery, - ): AsyncIterable { - let response = await this.requestApi(path, query); - - for (;;) { - if (response.status !== 200) { - await this.handleError(path, response); - } - - const result = await response.json(); - const elements: T[] = result.value; - - yield* elements; - - // Follow cursor to the next page if one is available - if (!result['@odata.nextLink']) { - return; - } - - response = await this.requestRaw(result['@odata.nextLink']); - } - } - - async requestApi(path: string, query?: ODataQuery): Promise { - const queryString = qs.stringify( - { - $filter: query?.filter, - $select: query?.select?.join(','), - $expand: query?.expand?.join(','), - }, - { - addQueryPrefix: true, - // Microsoft Graph doesn't like an encoded query string - encode: false, - }, - ); - - return await this.requestRaw(`${this.baseUrl}/${path}${queryString}`); - } - - async requestRaw(url: string): Promise { - // Make sure that we always have a valid access token (might be cached) - const token = await this.pca.acquireTokenByClientCredential({ - scopes: ['https://graph.microsoft.com/.default'], - }); - - if (!token) { - throw new Error('Error while requesting token for Microsoft Graph'); - } - - return await fetch(url, { - headers: { - Authorization: `Bearer ${token.accessToken}`, - }, - }); - } - - async getUserProfile(userId: string): Promise { - const response = await this.requestApi(`users/${userId}`); - - if (response.status !== 200) { - await this.handleError('user profile', response); - } - - return await response.json(); - } - - async getUserPhotoWithSizeLimit( - userId: string, - maxSize: number, - ): Promise { - return await this.getPhotoWithSizeLimit('users', userId, maxSize); - } - - async getUserPhoto( - userId: string, - sizeId?: string, - ): Promise { - return await this.getPhoto('users', userId, sizeId); - } - - async *getUsers(query?: ODataQuery): AsyncIterable { - yield* this.requestCollection(`users`, query); - } - - async getGroupPhotoWithSizeLimit( - groupId: string, - maxSize: number, - ): Promise { - return await this.getPhotoWithSizeLimit('groups', groupId, maxSize); - } - - async getGroupPhoto( - groupId: string, - sizeId?: string, - ): Promise { - return await this.getPhoto('groups', groupId, sizeId); - } - - async *getGroups(query?: ODataQuery): AsyncIterable { - yield* this.requestCollection(`groups`, query); - } - - async *getGroupMembers(groupId: string): AsyncIterable { - yield* this.requestCollection(`groups/${groupId}/members`); - } - - async getOrganization( - tenantId: string, - ): Promise { - const response = await this.requestApi(`organization/${tenantId}`); - - if (response.status !== 200) { - await this.handleError(`organization/${tenantId}`, response); - } - - return await response.json(); - } - - private async getPhotoWithSizeLimit( - entityName: string, - id: string, - maxSize: number, - ): Promise { - const response = await this.requestApi(`${entityName}/${id}/photos`); - - if (response.status === 404) { - return undefined; - } else if (response.status !== 200) { - await this.handleError(`${entityName} photos`, response); - } - - const result = await response.json(); - const photos = result.value as MicrosoftGraph.ProfilePhoto[]; - let selectedPhoto: MicrosoftGraph.ProfilePhoto | undefined = undefined; - - // Find the biggest picture that is smaller than the max size - for (const p of photos) { - if ( - !selectedPhoto || - (p.height! >= selectedPhoto.height! && p.height! <= maxSize) - ) { - selectedPhoto = p; - } - } - - if (!selectedPhoto) { - return undefined; - } - - return await this.getPhoto(entityName, id, selectedPhoto.id!); - } - - private async getPhoto( - entityName: string, - id: string, - sizeId?: string, - ): Promise { - const path = sizeId - ? `${entityName}/${id}/photos/${sizeId}/$value` - : `${entityName}/${id}/photo/$value`; - const response = await this.requestApi(path); - - if (response.status === 404) { - return undefined; - } else if (response.status !== 200) { - await this.handleError('photo', response); - } - - return `data:image/jpeg;base64,${Buffer.from( - await response.arrayBuffer(), - ).toString('base64')}`; - } - - private async handleError(path: string, response: Response): Promise { - const result = await response.json(); - const error = result.error as MicrosoftGraph.PublicError; - - throw new Error( - `Error while reading ${path} from Microsoft Graph: ${error.code} - ${error.message}`, - ); - } -} diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.test.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.test.ts deleted file mode 100644 index 4671fd23ae..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.test.ts +++ /dev/null @@ -1,75 +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 { ConfigReader } from '@backstage/config'; -import { readMicrosoftGraphConfig } from './config'; - -describe('readMicrosoftGraphConfig', () => { - it('applies all of the defaults', () => { - const config = { - providers: [ - { - target: 'target', - tenantId: 'tenantId', - clientId: 'clientId', - clientSecret: 'clientSecret', - }, - ], - }; - const actual = readMicrosoftGraphConfig(new ConfigReader(config)); - const expected = [ - { - target: 'target', - tenantId: 'tenantId', - clientId: 'clientId', - clientSecret: 'clientSecret', - authority: 'https://login.microsoftonline.com', - userFilter: undefined, - groupFilter: undefined, - }, - ]; - expect(actual).toEqual(expected); - }); - - it('reads all the values', () => { - const config = { - providers: [ - { - target: 'target', - tenantId: 'tenantId', - clientId: 'clientId', - clientSecret: 'clientSecret', - authority: 'https://login.example.com/', - userFilter: 'accountEnabled eq true', - groupFilter: 'securityEnabled eq false', - }, - ], - }; - const actual = readMicrosoftGraphConfig(new ConfigReader(config)); - const expected = [ - { - target: 'target', - tenantId: 'tenantId', - clientId: 'clientId', - clientSecret: 'clientSecret', - authority: 'https://login.example.com', - userFilter: 'accountEnabled eq true', - groupFilter: 'securityEnabled eq false', - }, - ]; - expect(actual).toEqual(expected); - }); -}); diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.ts deleted file mode 100644 index 72416a63ee..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.ts +++ /dev/null @@ -1,91 +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 { Config } from '@backstage/config'; - -/** - * The configuration parameters for a single Microsoft Graph provider. - */ -export type MicrosoftGraphProviderConfig = { - /** - * The prefix of the target that this matches on, e.g. - * "https://graph.microsoft.com/v1.0", with no trailing slash. - */ - target: string; - /** - * The auth authority used. - * - * E.g. "https://login.microsoftonline.com" - */ - authority?: string; - /** - * The tenant whose org data we are interested in. - */ - tenantId: string; - /** - * The OAuth client ID to use for authenticating requests. - */ - clientId: string; - /** - * The OAuth client secret to use for authenticating requests. - * - * @visibility secret - */ - clientSecret: string; - /** - * The filter to apply to extract users. - * - * E.g. "accountEnabled eq true and userType eq 'member'" - */ - userFilter?: string; - /** - * The filter to apply to extract groups. - * - * E.g. "securityEnabled eq false and mailEnabled eq true" - */ - groupFilter?: string; -}; - -export function readMicrosoftGraphConfig( - config: Config, -): MicrosoftGraphProviderConfig[] { - const providers: MicrosoftGraphProviderConfig[] = []; - const providerConfigs = config.getOptionalConfigArray('providers') ?? []; - - for (const providerConfig of providerConfigs) { - const target = providerConfig.getString('target').replace(/\/+$/, ''); - const authority = - providerConfig.getOptionalString('authority')?.replace(/\/+$/, '') || - 'https://login.microsoftonline.com'; - const tenantId = providerConfig.getString('tenantId'); - const clientId = providerConfig.getString('clientId'); - const clientSecret = providerConfig.getString('clientSecret'); - const userFilter = providerConfig.getOptionalString('userFilter'); - const groupFilter = providerConfig.getOptionalString('groupFilter'); - - providers.push({ - target, - authority, - tenantId, - clientId, - clientSecret, - userFilter, - groupFilter, - }); - } - - return providers; -} diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/constants.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/constants.ts deleted file mode 100644 index 6d34d0c159..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/constants.ts +++ /dev/null @@ -1,32 +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. - */ - -/** - * The tenant id used by the Microsoft Graph API - */ -export const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION = - 'graph.microsoft.com/tenant-id'; - -/** - * The group id used by the Microsoft Graph API - */ -export const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION = - 'graph.microsoft.com/group-id'; - -/** - * The user id used by the Microsoft Graph API - */ -export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id'; diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/index.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/index.ts deleted file mode 100644 index 882125fd84..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/index.ts +++ /dev/null @@ -1,24 +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 { MicrosoftGraphClient } from './client'; -export type { MicrosoftGraphProviderConfig } from './config'; -export { readMicrosoftGraphConfig } from './config'; -export { readMicrosoftGraphOrg } from './read'; -export { - MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, - MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, - MICROSOFT_GRAPH_USER_ID_ANNOTATION, -} from './constants'; diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.test.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.test.ts deleted file mode 100644 index 07d540c848..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.test.ts +++ /dev/null @@ -1,347 +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 { GroupEntity, UserEntity } from '@backstage/catalog-model'; -import merge from 'lodash/merge'; -import { RecursivePartial } from '../../../util'; -import { GroupMember, MicrosoftGraphClient } from './client'; -import { - normalizeEntityName, - readMicrosoftGraphGroups, - readMicrosoftGraphOrganization, - readMicrosoftGraphUsers, - resolveRelations, -} from './read'; - -function user(data: RecursivePartial): UserEntity { - return merge( - {}, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { name: 'name' }, - spec: { profile: {}, memberOf: [] }, - } as UserEntity, - data, - ); -} - -function group(data: RecursivePartial): GroupEntity { - return merge( - {}, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: 'name', - }, - spec: { - children: [], - type: 'team', - }, - } as GroupEntity, - data, - ); -} - -describe('read microsoft graph', () => { - const client: jest.Mocked = { - getUsers: jest.fn(), - getGroups: jest.fn(), - getGroupMembers: jest.fn(), - getUserPhotoWithSizeLimit: jest.fn(), - getGroupPhotoWithSizeLimit: jest.fn(), - getOrganization: jest.fn(), - } as any; - - afterEach(() => jest.resetAllMocks()); - - describe('normalizeEntityName', () => { - it('should normalize name to valid entity name', () => { - expect(normalizeEntityName('User Name')).toBe('user_name'); - }); - - it('should normalize e-mail to valid entity name', () => { - expect(normalizeEntityName('user.name@example.com')).toBe( - 'user.name_example.com', - ); - }); - }); - - describe('readMicrosoftGraphUsers', () => { - it('should read users', async () => { - async function* getExampleUsers() { - yield { - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }; - } - - client.getUsers.mockImplementation(getExampleUsers); - client.getUserPhotoWithSizeLimit.mockResolvedValue( - 'data:image/jpeg;base64,...', - ); - - const { users } = await readMicrosoftGraphUsers(client, { - userFilter: 'accountEnabled eq true', - }); - - expect(users).toEqual([ - user({ - metadata: { - annotations: { - 'graph.microsoft.com/user-id': 'userid', - }, - name: 'user.name_example.com', - }, - spec: { - profile: { - displayName: 'User Name', - email: 'user.name@example.com', - picture: 'data:image/jpeg;base64,...', - }, - }, - }), - ]); - - expect(client.getUsers).toBeCalledTimes(1); - expect(client.getUsers).toBeCalledWith({ - filter: 'accountEnabled eq true', - select: ['id', 'displayName', 'mail'], - }); - expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); - expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); - }); - }); - - describe('readMicrosoftGraphOrganization', () => { - it('should read organization', async () => { - client.getOrganization.mockResolvedValue({ - id: 'tenantid', - displayName: 'Organization Name', - }); - - const { rootGroup } = await readMicrosoftGraphOrganization( - client, - 'tenantid', - ); - - expect(rootGroup).toEqual( - group({ - metadata: { - annotations: { - 'graph.microsoft.com/tenant-id': 'tenantid', - }, - name: 'organization_name', - description: 'Organization Name', - }, - spec: { - type: 'root', - profile: { - displayName: 'Organization Name', - }, - }, - }), - ); - - expect(client.getOrganization).toBeCalledTimes(1); - expect(client.getOrganization).toBeCalledWith('tenantid'); - }); - }); - - describe('readMicrosoftGraphGroups', () => { - it('should read groups', async () => { - async function* getExampleGroups() { - yield { - id: 'groupid', - displayName: 'Group Name', - description: 'Group Description', - mail: 'group@example.com', - }; - } - - async function* getExampleGroupMembers(): AsyncIterable { - yield { - '@odata.type': '#microsoft.graph.group', - id: 'childgroupid', - }; - yield { - '@odata.type': '#microsoft.graph.user', - id: 'userid', - }; - } - - client.getGroups.mockImplementation(getExampleGroups); - client.getGroupMembers.mockImplementation(getExampleGroupMembers); - client.getOrganization.mockResolvedValue({ - id: 'tenantid', - displayName: 'Organization Name', - }); - client.getGroupPhotoWithSizeLimit.mockResolvedValue( - 'data:image/jpeg;base64,...', - ); - - const { - groups, - groupMember, - groupMemberOf, - rootGroup, - } = await readMicrosoftGraphGroups(client, 'tenantid', { - groupFilter: 'securityEnabled eq false', - }); - - const expectedRootGroup = group({ - metadata: { - annotations: { - 'graph.microsoft.com/tenant-id': 'tenantid', - }, - name: 'organization_name', - description: 'Organization Name', - }, - spec: { - type: 'root', - profile: { - displayName: 'Organization Name', - }, - }, - }); - expect(groups).toEqual([ - expectedRootGroup, - group({ - metadata: { - annotations: { - 'graph.microsoft.com/group-id': 'groupid', - }, - name: 'group_name', - description: 'Group Description', - }, - spec: { - type: 'team', - profile: { - displayName: 'Group Name', - email: 'group@example.com', - // TODO: Loading groups doesn't work right now as Microsoft Graph - // doesn't allows this yet - /* picture: 'data:image/jpeg;base64,...',*/ - }, - }, - }), - ]); - expect(rootGroup).toEqual(expectedRootGroup); - expect(groupMember.get('groupid')).toEqual(new Set(['childgroupid'])); - expect(groupMemberOf.get('userid')).toEqual(new Set(['groupid'])); - expect(groupMember.get('organization_name')).toEqual(new Set()); - - expect(client.getGroups).toBeCalledTimes(1); - expect(client.getGroups).toBeCalledWith({ - filter: 'securityEnabled eq false', - select: ['id', 'displayName', 'description', 'mail', 'mailNickname'], - }); - expect(client.getGroupMembers).toBeCalledTimes(1); - expect(client.getGroupMembers).toBeCalledWith('groupid'); - // TODO: Loading groups doesn't work right now as Microsoft Graph - // doesn't allows this yet - // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); - // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); - }); - }); - - describe('resolveRelations', () => { - it('should resolve relations', async () => { - const rootGroup = group({ - metadata: { - annotations: { - 'graph.microsoft.com/tenant-id': 'tenant-id-root', - }, - name: 'root', - }, - spec: { - type: 'root', - }, - }); - const groupA = group({ - metadata: { - annotations: { - 'graph.microsoft.com/group-id': 'group-id-a', - }, - name: 'a', - }, - }); - const groupB = group({ - metadata: { - annotations: { - 'graph.microsoft.com/group-id': 'group-id-b', - }, - name: 'b', - }, - }); - const groupC = group({ - metadata: { - annotations: { - 'graph.microsoft.com/group-id': 'group-id-c', - }, - name: 'c', - }, - }); - const user1 = user({ - metadata: { - annotations: { - 'graph.microsoft.com/user-id': 'user-id-1', - }, - name: 'user1', - }, - }); - const user2 = user({ - metadata: { - annotations: { - 'graph.microsoft.com/user-id': 'user-id-2', - }, - name: 'user2', - }, - }); - const groups = [rootGroup, groupA, groupB, groupC]; - const users = [user1, user2]; - const groupMember = new Map>(); - groupMember.set('group-id-b', new Set(['group-id-c'])); - const groupMemberOf = new Map>(); - groupMemberOf.set('user-id-1', new Set(['group-id-a'])); - groupMemberOf.set('user-id-2', new Set(['group-id-c'])); - - // We have a root groups - // We have three groups: a, b, c. c is child of b - // we have two users: u1, u2. u1 is member of a, u2 is member of c - resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); - - expect(rootGroup.spec.parent).toBeUndefined(); - expect(rootGroup.spec.children).toEqual( - expect.arrayContaining(['a', 'b']), - ); - - expect(groupA.spec.parent).toEqual('root'); - expect(groupA.spec.children).toEqual(expect.arrayContaining([])); - - expect(groupB.spec.parent).toEqual('root'); - expect(groupB.spec.children).toEqual(expect.arrayContaining(['c'])); - - expect(groupC.spec.parent).toEqual('b'); - expect(groupC.spec.children).toEqual(expect.arrayContaining([])); - - expect(user1.spec.memberOf).toEqual(expect.arrayContaining(['a'])); - expect(user2.spec.memberOf).toEqual(expect.arrayContaining(['b', 'c'])); - }); - }); -}); diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.ts deleted file mode 100644 index 4409422645..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.ts +++ /dev/null @@ -1,363 +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 { GroupEntity, UserEntity } from '@backstage/catalog-model'; -import limiterFactory from 'p-limit'; -import { buildMemberOf, buildOrgHierarchy } from '../util/org'; -import { MicrosoftGraphClient } from './client'; -import { - MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, - MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, - MICROSOFT_GRAPH_USER_ID_ANNOTATION, -} from './constants'; - -export function normalizeEntityName(name: string): string { - return name - .trim() - .toLocaleLowerCase() - .replace(/[^a-zA-Z0-9_\-\.]/g, '_'); -} - -export async function readMicrosoftGraphUsers( - client: MicrosoftGraphClient, - options?: { userFilter?: string }, -): Promise<{ - users: UserEntity[]; // With all relations empty -}> { - const entities: UserEntity[] = []; - const promises: Promise[] = []; - const limiter = limiterFactory(10); - - for await (const user of client.getUsers({ - filter: options?.userFilter, - select: ['id', 'displayName', 'mail'], - })) { - if (!user.id || !user.displayName || !user.mail) { - continue; - } - - const name = normalizeEntityName(user.mail); - const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name, - annotations: { - [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!, - }, - }, - spec: { - profile: { - displayName: user.displayName!, - email: user.mail!, - - // TODO: Additional fields? - // jobTitle: user.jobTitle || undefined, - // officeLocation: user.officeLocation || undefined, - // mobilePhone: user.mobilePhone || undefined, - }, - memberOf: [], - }, - }; - - // Download the photos in parallel, otherwise it can take quite some time - const loadPhoto = limiter(async () => { - entity.spec.profile!.picture = await client.getUserPhotoWithSizeLimit( - user.id!, - // We are limiting the photo size, as users with full resolution photos - // can make the Backstage API slow - 120, - ); - }); - - promises.push(loadPhoto); - entities.push(entity); - } - - // Wait for all photos to be downloaded - await Promise.all(promises); - - return { users: entities }; -} - -export async function readMicrosoftGraphOrganization( - client: MicrosoftGraphClient, - tenantId: string, -): Promise<{ - rootGroup: GroupEntity; // With all relations empty -}> { - // For now we expect a single root organization - const organization = await client.getOrganization(tenantId); - const name = normalizeEntityName(organization.displayName!); - const rootGroup: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: name, - description: organization.displayName!, - annotations: { - [MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]: organization.id!, - }, - }, - spec: { - type: 'root', - profile: { - displayName: organization.displayName!, - }, - children: [], - }, - }; - - return { rootGroup }; -} - -export async function readMicrosoftGraphGroups( - client: MicrosoftGraphClient, - tenantId: string, - options?: { groupFilter?: string }, -): Promise<{ - groups: GroupEntity[]; // With all relations empty - rootGroup: GroupEntity | undefined; // With all relations empty - groupMember: Map>; - groupMemberOf: Map>; -}> { - const groups: GroupEntity[] = []; - const groupMember: Map> = new Map(); - const groupMemberOf: Map> = new Map(); - const limiter = limiterFactory(10); - - const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId); - groupMember.set(rootGroup.metadata.name, new Set()); - groups.push(rootGroup); - - const promises: Promise[] = []; - - for await (const group of client.getGroups({ - filter: options?.groupFilter, - select: ['id', 'displayName', 'description', 'mail', 'mailNickname'], - })) { - if (!group.id || !group.displayName) { - continue; - } - - const name = normalizeEntityName(group.mailNickname || group.displayName); - const entity: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: name, - annotations: { - [MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id, - }, - }, - spec: { - type: 'team', - profile: {}, - children: [], - }, - }; - - if (group.description) { - entity.metadata.description = group.description; - } - if (group.displayName) { - entity.spec.profile!.displayName = group.displayName; - } - if (group.mail) { - entity.spec.profile!.email = group.mail; - } - - // Download the members in parallel, otherwise it can take quite some time - const loadGroupMembers = limiter(async () => { - for await (const member of client.getGroupMembers(group.id!)) { - if (!member.id) { - continue; - } - - if (member['@odata.type'] === '#microsoft.graph.user') { - ensureItem(groupMemberOf, member.id, group.id!); - } - - if (member['@odata.type'] === '#microsoft.graph.group') { - ensureItem(groupMember, group.id!, member.id); - } - } - }); - - // TODO: Loading groups doesn't work right now as Microsoft Graph doesn't - // allows this yet: https://microsoftgraph.uservoice.com/forums/920506-microsoft-graph-feature-requests/suggestions/37884922-allow-application-to-set-or-update-a-group-s-photo - /*/ / Download the photos in parallel, otherwise it can take quite some time - const loadPhoto = limiter(async () => { - entity.spec.profile!.picture = await client.getGroupPhotoWithSizeLimit( - group.id!, - // We are limiting the photo size, as groups with full resolution photos - // can make the Backstage API slow - 120, - ); - }); - - promises.push(loadPhoto);*/ - promises.push(loadGroupMembers); - groups.push(entity); - } - - // Wait for all group members and photos to be loaded - await Promise.all(promises); - - return { - groups, - rootGroup, - groupMember, - groupMemberOf, - }; -} - -export function resolveRelations( - rootGroup: GroupEntity | undefined, - groups: GroupEntity[], - users: UserEntity[], - groupMember: Map>, - groupMemberOf: Map>, -) { - // Build reference lookup tables, we reference them by the id the the graph - const groupMap: Map = new Map(); // by group-id or tenant-id - - for (const group of groups) { - if (group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]) { - groupMap.set( - group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION], - group, - ); - } - if (group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]) { - groupMap.set( - group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION], - group, - ); - } - } - - // Resolve all member relationships into the reverse direction - const parentGroups = new Map>(); - - groupMember.forEach((members, groupId) => - members.forEach(m => ensureItem(parentGroups, m, groupId)), - ); - - // Make sure every group (except root) has at least one parent. If the parent is missing, add the root. - if (rootGroup) { - const tenantId = rootGroup.metadata.annotations![ - MICROSOFT_GRAPH_TENANT_ID_ANNOTATION - ]; - - groups.forEach(group => { - const groupId = group.metadata.annotations![ - MICROSOFT_GRAPH_GROUP_ID_ANNOTATION - ]; - - if (!groupId) { - return; - } - - if (retrieveItems(parentGroups, groupId).size === 0) { - ensureItem(parentGroups, groupId, tenantId); - ensureItem(groupMember, tenantId, groupId); - } - }); - } - - groups.forEach(group => { - const id = - group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] ?? - group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]; - - retrieveItems(groupMember, id).forEach(m => { - const childGroup = groupMap.get(m); - if (childGroup) { - group.spec.children.push(childGroup.metadata.name); - } - }); - - retrieveItems(parentGroups, id).forEach(p => { - const parentGroup = groupMap.get(p); - if (parentGroup) { - // TODO: Only having a single parent group might not match every companies model, but fine for now. - group.spec.parent = parentGroup.metadata.name; - } - }); - }); - - // Make sure that all groups have proper parents and children - buildOrgHierarchy(groups); - - // Set relations for all users - users.forEach(user => { - const id = user.metadata.annotations![MICROSOFT_GRAPH_USER_ID_ANNOTATION]; - - retrieveItems(groupMemberOf, id).forEach(p => { - const parentGroup = groupMap.get(p); - if (parentGroup) { - user.spec.memberOf.push(parentGroup.metadata.name); - } - }); - }); - - // Make sure all transitive memberships are available - buildMemberOf(groups, users); -} - -export async function readMicrosoftGraphOrg( - client: MicrosoftGraphClient, - tenantId: string, - options?: { userFilter?: string; groupFilter?: string }, -): Promise<{ users: UserEntity[]; groups: GroupEntity[] }> { - const { users } = await readMicrosoftGraphUsers(client, { - userFilter: options?.userFilter, - }); - const { - groups, - rootGroup, - groupMember, - groupMemberOf, - } = await readMicrosoftGraphGroups(client, tenantId, { - groupFilter: options?.groupFilter, - }); - - resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); - users.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); - groups.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); - - return { users, groups }; -} - -function ensureItem( - target: Map>, - key: string, - value: string, -) { - let set = target.get(key); - if (!set) { - set = new Set(); - target.set(key, set); - } - set!.add(value); -} - -function retrieveItems( - target: Map>, - key: string, -): Set { - return target.get(key) ?? new Set(); -} diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index 2551c16879..0bb5cd0aee 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -50,7 +50,6 @@ import { GithubDiscoveryProcessor, GithubOrgReaderProcessor, LdapOrgReaderProcessor, - MicrosoftGraphOrgReaderProcessor, PlaceholderProcessor, PlaceholderResolver, UrlReaderProcessor, @@ -373,7 +372,6 @@ export class NextCatalogBuilder { GithubDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), LdapOrgReaderProcessor.fromConfig(config, { logger }), - MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }), new UrlReaderProcessor({ reader, logger }), CodeOwnersProcessor.fromConfig(config, { logger, reader }), new AnnotateLocationEntityProcessor({ integrations }), diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 9bec4442d7..1901d18f72 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -51,7 +51,6 @@ import { LdapOrgReaderProcessor, LocationEntityProcessor, LocationReaders, - MicrosoftGraphOrgReaderProcessor, PlaceholderProcessor, PlaceholderResolver, StaticLocationProcessor, @@ -319,7 +318,6 @@ export class CatalogBuilder { GithubDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), LdapOrgReaderProcessor.fromConfig(config, { logger }), - MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }), new UrlReaderProcessor({ reader, logger }), CodeOwnersProcessor.fromConfig(config, { logger, reader }), new LocationEntityProcessor({ integrations }), diff --git a/yarn.lock b/yarn.lock index ab538fb58a..0b1af46a72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -225,7 +225,7 @@ dependencies: debug "^4.1.1" -"@azure/msal-node@1.0.0-beta.3", "@azure/msal-node@^1.0.0-beta.3": +"@azure/msal-node@1.0.0-beta.3": version "1.0.0-beta.3" resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.3.tgz#c84c7948028b39e48b901f5fac35bdedcbc8772e" integrity sha512-/KfYRfrsOIrZONvo/0Vi5umuqbPBtCWNtmRvkse64uI0C4CP/W4WXwRD42VMws/8LtKvr1I5rYlYgFzt5zDz/A== From 84cb4410eb87d6383aad57cc0faf8447b4802653 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 16 Jun 2021 11:35:38 +0200 Subject: [PATCH 200/206] Format sidebars.json Signed-off-by: Oliver Sand --- microsite/sidebars.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 9021d15884..ea1e1a4e89 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -103,10 +103,7 @@ { "type": "subcategory", "label": "Azure", - "ids": [ - "integrations/azure/locations", - "integrations/azure/org" - ] + "ids": ["integrations/azure/locations", "integrations/azure/org"] }, { "type": "subcategory", From d6fb1cf1ba56905b40a9034787e8d51afacdc611 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 17 Jun 2021 17:37:58 +0200 Subject: [PATCH 201/206] Upgrade packages Signed-off-by: Oliver Sand --- packages/backend/package.json | 1 - plugins/catalog-backend-module-msgraph/package.json | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 5195a7c28f..f4f304d714 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -31,7 +31,6 @@ "@backstage/catalog-client": "^0.3.13", "@backstage/catalog-model": "^0.8.2", "@backstage/config": "^0.1.5", - "@backstage/integration": "^0.5.6", "@backstage/plugin-app-backend": "^0.3.13", "@backstage/plugin-auth-backend": "^0.3.12", "@backstage/plugin-badges-backend": "^0.1.6", diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 65794cbd91..83bb5dd802 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -29,7 +29,7 @@ }, "dependencies": { "@azure/msal-node": "^1.1.0", - "@backstage/catalog-model": "^0.8.2", + "@backstage/catalog-model": "^0.8.3", "@backstage/config": "^0.1.5", "@backstage/plugin-catalog-backend": "^0.10.2", "@microsoft/microsoft-graph-types": "^1.25.0", @@ -40,7 +40,7 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.1", "@backstage/test-utils": "^0.1.13", "@types/lodash": "^4.14.151", "msw": "^0.21.2" From 79ec37d1bafed9f20ffdf000a79171720ad3b3de Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 18 Jun 2021 09:48:26 +0200 Subject: [PATCH 202/206] Update api reports Signed-off-by: Oliver Sand --- .github/styles/vocab.txt | 1 + .../api-report.md | 121 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 plugins/catalog-backend-module-msgraph/api-report.md diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 4ff766e5e2..78b2a9b75f 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -149,6 +149,7 @@ Mkdocs monorepo Monorepo monorepos +msgraph msw mysql namespace diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md new file mode 100644 index 0000000000..702601b469 --- /dev/null +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -0,0 +1,121 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-msgraph" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; +import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; +import { Config } from '@backstage/config'; +import { GroupEntity } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/catalog-model'; +import { Logger } from 'winston'; +import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; +import * as msal from '@azure/msal-node'; +import { UserEntity } from '@backstage/catalog-model'; + +// @public (undocumented) +export function defaultGroupTransformer(group: MicrosoftGraph.Group, groupPhoto?: string): Promise; + +// @public (undocumented) +export function defaultOrganizationTransformer(organization: MicrosoftGraph.Organization): Promise; + +// @public (undocumented) +export function defaultUserTransformer(user: MicrosoftGraph.User, userPhoto?: string): Promise; + +// @public (undocumented) +export type GroupTransformer = (group: MicrosoftGraph.Group, groupPhoto?: string) => Promise; + +// @public +export const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION = "graph.microsoft.com/group-id"; + +// @public +export const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION = "graph.microsoft.com/tenant-id"; + +// @public +export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = "graph.microsoft.com/user-id"; + +// @public (undocumented) +export class MicrosoftGraphClient { + constructor(baseUrl: string, pca: msal.ConfidentialClientApplication); + // (undocumented) + static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient; + // (undocumented) + getGroupMembers(groupId: string): AsyncIterable; + // (undocumented) + getGroupPhoto(groupId: string, sizeId?: string): Promise; + // (undocumented) + getGroupPhotoWithSizeLimit(groupId: string, maxSize: number): Promise; + // (undocumented) + getGroups(query?: ODataQuery): AsyncIterable; + // (undocumented) + getOrganization(tenantId: string): Promise; + // (undocumented) + getUserPhoto(userId: string, sizeId?: string): Promise; + // (undocumented) + getUserPhotoWithSizeLimit(userId: string, maxSize: number): Promise; + // (undocumented) + getUserProfile(userId: string): Promise; + // (undocumented) + getUsers(query?: ODataQuery): AsyncIterable; + // (undocumented) + requestApi(path: string, query?: ODataQuery): Promise; + // (undocumented) + requestCollection(path: string, query?: ODataQuery): AsyncIterable; + // (undocumented) + requestRaw(url: string): Promise; +} + +// @public +export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { + constructor(options: { + providers: MicrosoftGraphProviderConfig[]; + logger: Logger; + groupTransformer?: GroupTransformer; + }); + // (undocumented) + static fromConfig(config: Config, options: { + logger: Logger; + groupTransformer?: GroupTransformer; + }): MicrosoftGraphOrgReaderProcessor; + // (undocumented) + readLocation(location: LocationSpec, _optional: boolean, emit: CatalogProcessorEmit): Promise; +} + +// @public +export type MicrosoftGraphProviderConfig = { + target: string; + authority?: string; + tenantId: string; + clientId: string; + clientSecret: string; + userFilter?: string; + groupFilter?: string; +}; + +// @public (undocumented) +export function normalizeEntityName(name: string): string; + +// @public (undocumented) +export type OrganizationTransformer = (organization: MicrosoftGraph.Organization) => Promise; + +// @public (undocumented) +export function readMicrosoftGraphConfig(config: Config): MicrosoftGraphProviderConfig[]; + +// @public (undocumented) +export function readMicrosoftGraphOrg(client: MicrosoftGraphClient, tenantId: string, options?: { + userFilter?: string; + groupFilter?: string; + groupTransformer?: GroupTransformer; +}): Promise<{ + users: UserEntity[]; + groups: GroupEntity[]; +}>; + +// @public (undocumented) +export type UserTransformer = (user: MicrosoftGraph.User, userPhoto?: string) => Promise; + + +// (No @packageDocumentation comment for this package) + +``` From e97c49f0fcc60bd750840725319f9b520203eea6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jun 2021 08:31:33 +0000 Subject: [PATCH 203/206] chore(deps): bump @spotify/eslint-config-typescript from 9.0.0 to 10.0.0 Bumps [@spotify/eslint-config-typescript](https://github.com/spotify/web-scripts) from 9.0.0 to 10.0.0. - [Release notes](https://github.com/spotify/web-scripts/releases) - [Changelog](https://github.com/spotify/web-scripts/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotify/web-scripts/compare/v9.0.0...v10.0.0) --- updated-dependencies: - dependency-name: "@spotify/eslint-config-typescript" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 4d3ed7f4df..a9f34cb7ac 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -43,7 +43,7 @@ "@rollup/plugin-yaml": "^2.1.1", "@spotify/eslint-config-base": "^9.0.0", "@spotify/eslint-config-react": "^10.0.0", - "@spotify/eslint-config-typescript": "^9.0.0", + "@spotify/eslint-config-typescript": "^10.0.0", "@sucrase/jest-plugin": "^2.1.0", "@sucrase/webpack-loader": "^2.0.0", "@svgr/plugin-jsx": "5.5.x", diff --git a/yarn.lock b/yarn.lock index 208ad9e952..2a1f07047b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1347,7 +1347,7 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.7.4": - version "0.8.2" + version "0.8.3" dependencies: "@backstage/config" "^0.1.5" "@backstage/errors" "^0.1.1" @@ -1360,7 +1360,7 @@ yup "^0.29.3" "@backstage/catalog-model@^0.7.9": - version "0.8.2" + version "0.8.3" dependencies: "@backstage/config" "^0.1.5" "@backstage/errors" "^0.1.1" @@ -1389,16 +1389,16 @@ react-use "^17.2.4" "@backstage/plugin-catalog@^0.5.1": - version "0.6.2" + version "0.6.3" dependencies: "@backstage/catalog-client" "^0.3.13" - "@backstage/catalog-model" "^0.8.2" - "@backstage/core" "^0.7.12" + "@backstage/catalog-model" "^0.8.3" + "@backstage/core" "^0.7.13" "@backstage/core-plugin-api" "^0.1.2" "@backstage/errors" "^0.1.1" "@backstage/integration" "^0.5.6" "@backstage/integration-react" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.2.2" + "@backstage/plugin-catalog-react" "^0.2.3" "@backstage/theme" "^0.2.8" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -4227,10 +4227,10 @@ resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-10.0.0.tgz#6f83ada05f79b49c1f9def5b8815e3231ed24969" integrity sha512-MozX6W3aMp7EQPliuUQYI58Ni5vh65mItXMG0CgZBj0v1ZEeZVM5XS/nqhsCaIHYXskmZM2O1qqLFaEg5PqGdg== -"@spotify/eslint-config-typescript@^9.0.0": - version "9.0.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-9.0.0.tgz#be68cfaf212599f0bfeb6536c7c58ec05d2b6fba" - integrity sha512-ZsXTwMA68ZCz943U4N8XwprdWcc7ErOO/IW8PewLK5lycCZtLnmRkOvAbae7O5qNJPD8b/l0iUMTLaZuwjXWwg== +"@spotify/eslint-config-typescript@^10.0.0": + version "10.0.0" + resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-10.0.0.tgz#4df7074f3f4ef31d76c617e55d335f9a36cfed5b" + integrity sha512-qR4WOU3gJrpz26O8BlNbXas4Yj93NeVH7yvULVYO2j9bCAEZJu2sfl1BGfOy4qAsYGutZhJtNwMqK0Rl4DcFHQ== "@spotify/prettier-config@^10.0.0": version "10.0.0" From 176f28b712eedcee2ba08cbfc8f21f28a23fdb9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jun 2021 08:32:15 +0000 Subject: [PATCH 204/206] chore(deps): bump @google-cloud/container from 2.2.2 to 2.3.0 Bumps [@google-cloud/container](https://github.com/googleapis/nodejs-cloud-container) from 2.2.2 to 2.3.0. - [Release notes](https://github.com/googleapis/nodejs-cloud-container/releases) - [Changelog](https://github.com/googleapis/nodejs-cloud-container/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/nodejs-cloud-container/compare/v2.2.2...v2.3.0) --- updated-dependencies: - dependency-name: "@google-cloud/container" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 208ad9e952..012d1a1a9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1347,7 +1347,7 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.7.4": - version "0.8.2" + version "0.8.3" dependencies: "@backstage/config" "^0.1.5" "@backstage/errors" "^0.1.1" @@ -1360,7 +1360,7 @@ yup "^0.29.3" "@backstage/catalog-model@^0.7.9": - version "0.8.2" + version "0.8.3" dependencies: "@backstage/config" "^0.1.5" "@backstage/errors" "^0.1.1" @@ -1389,16 +1389,16 @@ react-use "^17.2.4" "@backstage/plugin-catalog@^0.5.1": - version "0.6.2" + version "0.6.3" dependencies: "@backstage/catalog-client" "^0.3.13" - "@backstage/catalog-model" "^0.8.2" - "@backstage/core" "^0.7.12" + "@backstage/catalog-model" "^0.8.3" + "@backstage/core" "^0.7.13" "@backstage/core-plugin-api" "^0.1.2" "@backstage/errors" "^0.1.1" "@backstage/integration" "^0.5.6" "@backstage/integration-react" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.2.2" + "@backstage/plugin-catalog-react" "^0.2.3" "@backstage/theme" "^0.2.8" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -1890,9 +1890,9 @@ teeny-request "^7.0.0" "@google-cloud/container@^2.2.0": - version "2.2.2" - resolved "https://registry.npmjs.org/@google-cloud/container/-/container-2.2.2.tgz#2b02e2cd3a446cfde3189c6018fad00244767b5b" - integrity sha512-r5DBAqKZtbU+DF/WLjldQfIuXiVLBBZJ7lJ/rrg9z20CvhFmv+ATVLkadh8018I2Xggor8+0ePp2Q6xstyFwMA== + version "2.3.0" + resolved "https://registry.npmjs.org/@google-cloud/container/-/container-2.3.0.tgz#a23f046948dbaf8cced008d419580cb600334efc" + integrity sha512-Tv8fR7JjlZr3oh476hMsf9yqGXbb/+81n0Va1Uc3reWjAdUXCYztH3/o/HMvh6yvd06j8VLLUxyBwAIb5PtW5g== dependencies: google-gax "^2.12.0" From 93529a5f11c53066ddd96d48bfd7073ae955a25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 18 Jun 2021 10:35:47 +0200 Subject: [PATCH 205/206] Permit, and prefer, "The Backstage Authors" in the copyright header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .eslintrc.js | 6 ++++++ scripts/copyright-header.txt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 4dfcf5317f..c8108f7289 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,6 +25,12 @@ module.exports = { { // eslint-disable-next-line no-restricted-syntax templateFile: path.resolve(__dirname, './scripts/copyright-header.txt'), + templateVars: { + NAME: 'The Backstage Authors', + }, + varRegexps: { + NAME: /(The Backstage Authors)|(Spotify AB)/, + }, onNonMatchingHeader: 'replace', }, ], diff --git a/scripts/copyright-header.txt b/scripts/copyright-header.txt index 4376d55847..478635997f 100644 --- a/scripts/copyright-header.txt +++ b/scripts/copyright-header.txt @@ -1,5 +1,5 @@ /* - * Copyright <%= YEAR %> Spotify AB + * Copyright <%= YEAR %> <%= NAME %> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From ece2b5dd1d0e9068761dd8eddb5863fba3350780 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Fri, 18 Jun 2021 11:19:06 +0200 Subject: [PATCH 206/206] Added changeset Signed-off-by: Ben Lambert --- .changeset/rich-trees-chew.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rich-trees-chew.md diff --git a/.changeset/rich-trees-chew.md b/.changeset/rich-trees-chew.md new file mode 100644 index 0000000000..5ec79c36d0 --- /dev/null +++ b/.changeset/rich-trees-chew.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +chore: bump `@spotify/eslint-config-typescript` from 9.0.0 to 10.0.0