From 84364b35c850a3fbee5a77383725b6107c69c4a0 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 28 Jan 2021 13:38:55 +0200 Subject: [PATCH 001/187] Added support for github-discovery locations --- .changeset/moody-mice-cheer.md | 5 + .../GithubDiscoveryProcessor.test.ts | 190 ++++++++++++++++++ .../processors/GithubDiscoveryProcessor.ts | 130 ++++++++++++ .../processors/github/github.test.ts | 45 +++++ .../src/ingestion/processors/github/github.ts | 46 ++++- .../src/ingestion/processors/github/index.ts | 6 +- 6 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 .changeset/moody-mice-cheer.md create mode 100644 plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts diff --git a/.changeset/moody-mice-cheer.md b/.changeset/moody-mice-cheer.md new file mode 100644 index 0000000000..bdbb0722b0 --- /dev/null +++ b/.changeset/moody-mice-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added an option to scan github for repositories using a new location type 'github-discovery' diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts new file mode 100644 index 0000000000..a3713fb374 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts @@ -0,0 +1,190 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { LocationSpec } from '@backstage/catalog-model'; +import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; +import { getOrganizationRepositories } from './github'; + +jest.mock('./github'); +const mockGetOrganizationRepositories = getOrganizationRepositories as jest.MockedFunction< + typeof getOrganizationRepositories +>; + +describe('GithubOrgReaderProcessor', () => { + describe('parseUrl', () => { + it('parses well formed URLs', () => { + expect( + parseUrl('https://github.com/foo/proj/blob/master/catalog.yaml'), + ).toEqual({ + org: 'foo', + repoSearchPath: /proj/, + catalogPath: 'blob/master/catalog.yaml', + }); + expect( + parseUrl('https://github.com/foo/proj*/blob/master/catalog.yaml'), + ).toEqual({ + org: 'foo', + repoSearchPath: /proj.*/, + catalogPath: 'blob/master/catalog.yaml', + }); + }); + + it('throws on incorrectly formed URLs', () => { + expect(() => parseUrl('https://github.com')).toThrow(); + expect(() => parseUrl('https://github.com//')).toThrow(); + expect(() => parseUrl('https://github.com/foo')).toThrow(); + expect(() => parseUrl('https://github.com//foo')).toThrow(); + expect(() => parseUrl('https://github.com/org/teams')).toThrow(); + expect(() => parseUrl('https://github.com/org//teams')).toThrow(); + }); + }); + + describe('reject unrelated entries', () => { + it('rejects unknown types', async () => { + const processor = new GithubDiscoveryProcessor({ + providers: [ + { + target: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + }, + ], + logger: getVoidLogger(), + }); + const location: LocationSpec = { + type: 'not-github-discovery', + target: 'https://github.com', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).resolves.toBeFalsy(); + }); + + it('rejects unknown targets', async () => { + const processor = new GithubDiscoveryProcessor({ + providers: [ + { + target: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + }, + ], + logger: getVoidLogger(), + }); + const location: LocationSpec = { + type: 'github-discovery', + target: 'https://not.github.com/apa', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).rejects.toThrow( + /There is no GitHub Org provider that matches https:\/\/not.github.com\/apa/, + ); + }); + }); + + describe('handles repositories', () => { + const processor = new GithubDiscoveryProcessor({ + providers: [ + { + target: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + }, + ], + logger: getVoidLogger(), + }); + + beforeEach(() => { + mockGetOrganizationRepositories.mockClear(); + }); + + it('output all repositories', async () => { + const location: LocationSpec = { + type: 'github-discovery', + target: 'https://github.com/backstage/*/blob/master/catalog.yaml', + }; + mockGetOrganizationRepositories.mockResolvedValueOnce({ + repositories: [ + { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { name: 'demo', url: 'https://github.com/backstage/demo' }, + ], + }); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://github.com/backstage/backstage/blob/master/catalog.yaml', + }, + optional: false, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: 'https://github.com/backstage/demo/blob/master/catalog.yaml', + }, + optional: false, + }); + }); + + it('filter unrelated repositories', async () => { + const location: LocationSpec = { + type: 'github-discovery', + target: + 'https://github.com/backstage/techdocs-*/blob/master/catalog.yaml', + }; + mockGetOrganizationRepositories.mockResolvedValueOnce({ + repositories: [ + { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { + name: 'techdocs-cli', + url: 'https://github.com/backstage/techdocs-cli', + }, + { + name: 'techdocs-container', + url: 'https://github.com/backstage/techdocs-container', + }, + ], + }); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://github.com/backstage/techdocs-cli/blob/master/catalog.yaml', + }, + optional: false, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://github.com/backstage/techdocs-container/blob/master/catalog.yaml', + }, + optional: false, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts new file mode 100644 index 0000000000..e5590b6179 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -0,0 +1,130 @@ +/* + * 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 { LocationSpec } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { graphql } from '@octokit/graphql'; +import { Logger } from 'winston'; +import { + getOrganizationRepositories, + ProviderConfig, + readGithubConfig, +} from './github'; +import * as results from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; + +/** + * Extracts teams and users out of a GitHub org. + */ +export class GithubDiscoveryProcessor implements CatalogProcessor { + private readonly providers: ProviderConfig[]; + private readonly logger: Logger; + + static fromConfig(config: Config, options: { logger: Logger }) { + return new GithubDiscoveryProcessor({ + ...options, + providers: readGithubConfig(config), + }); + } + + constructor(options: { providers: ProviderConfig[]; logger: Logger }) { + this.providers = options.providers; + this.logger = options.logger; + } + + async readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise { + if (location.type !== 'github-discovery') { + return false; + } + + const provider = this.providers.find(p => + location.target.startsWith(`${p.target}/`), + ); + if (!provider) { + throw new Error( + `There is no GitHub Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.githubOrg.providers.`, + ); + } + + const { org, repoSearchPath, catalogPath } = parseUrl(location.target); + const client = !provider.token + ? graphql + : graphql.defaults({ + baseUrl: provider.apiBaseUrl, + headers: { + authorization: `token ${provider.token}`, + }, + }); + + // Read out all of the raw data + const startTimestamp = Date.now(); + this.logger.info('Reading GitHub repositories'); + + const { repositories } = await getOrganizationRepositories(client, org); + + const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); + this.logger.debug( + `Read ${repositories.length} GitHub repositories in ${duration} seconds`, + ); + + for (const repository of repositories) { + if (!repoSearchPath.test(repository.name)) { + continue; + } + emit( + results.location( + { + type: 'url', + target: `${repository.url}/${catalogPath}`, + }, + false, + ), + ); + } + + return true; + } +} + +/* + * Helpers + */ + +export function parseUrl( + urlString: string, +): { org: string; repoSearchPath: RegExp; catalogPath: string } { + const url = new URL(urlString); + const path = url.pathname.substr(1).split('/'); + + // /backstage/techdocs-*/blob/master/catalog-info.yaml + if (path.length > 2 && path[0].length && path[1].length) { + return { + org: decodeURIComponent(path[0]), + repoSearchPath: escapeRegExp(decodeURIComponent(path[1])), + catalogPath: decodeURIComponent(path.slice(2).join('/')), + }; + } + + throw new Error(`Failed to parse ${urlString}`); +} + +export function escapeRegExp(str: string): RegExp { + return new RegExp(str.replace(/\*/g, '.*')); +} diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts index 8911e84d99..81b44c706d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts @@ -22,6 +22,7 @@ import { getOrganizationTeams, getOrganizationUsers, getTeamMembers, + getOrganizationRepositories, QueryResponse, } from './github'; @@ -151,4 +152,48 @@ describe('github', () => { await expect(getTeamMembers(graphql, 'a', 'b')).resolves.toEqual(output); }); }); + + describe('getOrganizationRepositories', () => { + it('read repositories', async () => { + const input: QueryResponse = { + organization: { + repositories: { + nodes: [ + { + name: 'backstage', + url: 'https://github.com/backstage/backstage', + }, + { + name: 'demo', + url: 'https://github.com/backstage/demo', + }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + }, + }; + + const output = { + repositories: [ + { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { + name: 'demo', + url: 'https://github.com/backstage/demo', + }, + ], + }; + + server.use( + graphqlMsw.query('repositories', (_req, res, ctx) => + res(ctx.data(input)), + ), + ); + + await expect(getOrganizationRepositories(graphql, 'a')).resolves.toEqual( + output, + ); + }); + }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.ts index 2b33d72f65..cd4719c46f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.ts @@ -14,8 +14,13 @@ * limitations under the License. */ -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { + GroupEntity, + LocationEntity, + UserEntity, +} from '@backstage/catalog-model'; import { graphql } from '@octokit/graphql'; +import { xorWith } from 'lodash/fp'; // Graphql types @@ -27,6 +32,7 @@ export type Organization = { membersWithRole?: Connection; team?: Team; teams?: Connection; + repositories?: Connection; }; export type PageInfo = { @@ -52,6 +58,11 @@ export type Team = { members: Connection; }; +export type Repository = { + name: string; + url: string; +}; + export type Connection = { pageInfo: PageInfo; nodes: T[]; @@ -216,6 +227,39 @@ export async function getOrganizationTeams( return { groups, groupMemberUsers }; } +export async function getOrganizationRepositories( + client: typeof graphql, + org: string, +): Promise<{ repositories: Repository[] }> { + const query = ` + query repositories($org: String!, $cursor: String) { + organization(login: $org) { + name + repositories(first: 100, after: $cursor) { + nodes { + name + url + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `; + + const repositories = await queryWithPaging( + client, + query, + r => r.organization?.repositories, + x => x, + { org }, + ); + + return { repositories }; +} + /** * Gets all the users out of a GitHub organization. * diff --git a/plugins/catalog-backend/src/ingestion/processors/github/index.ts b/plugins/catalog-backend/src/ingestion/processors/github/index.ts index e424d7bafb..2063e8c1b2 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/index.ts @@ -16,4 +16,8 @@ export { readGithubConfig } from './config'; export type { ProviderConfig } from './config'; -export { getOrganizationTeams, getOrganizationUsers } from './github'; +export { + getOrganizationTeams, + getOrganizationUsers, + getOrganizationRepositories, +} from './github'; From af045e3a77ac63775c9ed70cc9b2d77f2c26d675 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 28 Jan 2021 14:37:44 +0200 Subject: [PATCH 002/187] Extended changelist docs; Fixed lint errors --- .changeset/moody-mice-cheer.md | 12 +++++++++++- .../src/ingestion/processors/github/github.ts | 7 +------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.changeset/moody-mice-cheer.md b/.changeset/moody-mice-cheer.md index bdbb0722b0..f2e5d29ba1 100644 --- a/.changeset/moody-mice-cheer.md +++ b/.changeset/moody-mice-cheer.md @@ -2,4 +2,14 @@ '@backstage/plugin-catalog-backend': minor --- -Added an option to scan github for repositories using a new location type 'github-discovery' +Added an option to scan GitHub for repositories using a new location type `github-discovery`. +Example: + +```yaml +type: 'github-discovery', +target: + 'https://github.com/backstage/techdocs-*/blob/master/catalog.yaml' +``` + +You can use wildcards (`*`) as well. This will add `location` entities for each matching repository. +Currently though, you must specify the exact path of the `catalog.yaml` file in the repository. diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.ts index cd4719c46f..d50887c592 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.ts @@ -14,13 +14,8 @@ * limitations under the License. */ -import { - GroupEntity, - LocationEntity, - UserEntity, -} from '@backstage/catalog-model'; +import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import { graphql } from '@octokit/graphql'; -import { xorWith } from 'lodash/fp'; // Graphql types From 5f305b914132d4cb8aff1c6a99fd569f4242b72c Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 28 Jan 2021 15:00:16 +0200 Subject: [PATCH 003/187] Registered GithubDiscoveryProcessor in CatalogBuilder --- plugins/catalog-backend/src/ingestion/processors/index.ts | 1 + plugins/catalog-backend/src/service/CatalogBuilder.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index a7b00d7065..77dfb67687 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -21,6 +21,7 @@ export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAcco export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; +export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; export { LocationRefProcessor } from './LocationEntityProcessor'; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index f6170135fd..22865be66d 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -41,6 +41,7 @@ import { CatalogProcessor, CodeOwnersProcessor, FileReaderProcessor, + GithubDiscoveryProcessor, GithubOrgReaderProcessor, HigherOrderOperation, HigherOrderOperations, @@ -281,6 +282,7 @@ export class CatalogBuilder { if (!this.processorsReplace) { processors.push( new FileReaderProcessor(), + GithubDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), LdapOrgReaderProcessor.fromConfig(config, { logger }), MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }), From 7413148c5f53683d5321227a7fb27a5fa8739b68 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Fri, 29 Jan 2021 19:04:34 +0200 Subject: [PATCH 004/187] Changed to use Github integration configs --- plugins/catalog-backend/package.json | 1 + .../GithubDiscoveryProcessor.test.ts | 54 ++++++++++++------- .../processors/GithubDiscoveryProcessor.ts | 54 +++++++++++-------- yarn.lock | 49 +++++++++++++---- 4 files changed, 106 insertions(+), 52 deletions(-) diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index b2db27770b..07fd000618 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -34,6 +34,7 @@ "@backstage/backend-common": "^0.5.0", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", + "@backstage/integration": "^0.3.1", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", "@types/ldapjs": "^1.0.9", diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts index a3713fb374..9daecb26d3 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts @@ -56,12 +56,15 @@ describe('GithubOrgReaderProcessor', () => { describe('reject unrelated entries', () => { it('rejects unknown types', async () => { const processor = new GithubDiscoveryProcessor({ - providers: [ - { - target: 'https://github.com', - apiBaseUrl: 'https://api.github.com', - }, - ], + gitHubConfigMap: new Map([ + [ + 'github.com', + { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }, + ], + ]), logger: getVoidLogger(), }); const location: LocationSpec = { @@ -75,12 +78,22 @@ describe('GithubOrgReaderProcessor', () => { it('rejects unknown targets', async () => { const processor = new GithubDiscoveryProcessor({ - providers: [ - { - target: 'https://github.com', - apiBaseUrl: 'https://api.github.com', - }, - ], + gitHubConfigMap: new Map([ + [ + 'github.com', + { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }, + ], + [ + 'ghe.example.net', + { + host: 'ghe.example.net', + apiBaseUrl: 'https://ghe.example.net/api/v3', + }, + ], + ]), logger: getVoidLogger(), }); const location: LocationSpec = { @@ -90,19 +103,22 @@ describe('GithubOrgReaderProcessor', () => { await expect( processor.readLocation(location, false, () => {}), ).rejects.toThrow( - /There is no GitHub Org provider that matches https:\/\/not.github.com\/apa/, + /There is no GitHub integration that matches https:\/\/not.github.com\/apa/, ); }); }); describe('handles repositories', () => { const processor = new GithubDiscoveryProcessor({ - providers: [ - { - target: 'https://github.com', - apiBaseUrl: 'https://api.github.com', - }, - ], + gitHubConfigMap: new Map([ + [ + 'github.com', + { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }, + ], + ]), logger: getVoidLogger(), }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts index e5590b6179..698f0ad987 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -16,32 +16,41 @@ import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +import { + GithubCredentialsProvider, + GitHubIntegrationConfig, + readGitHubIntegrationConfigs, +} from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { Logger } from 'winston'; -import { - getOrganizationRepositories, - ProviderConfig, - readGithubConfig, -} from './github'; +import { getOrganizationRepositories } from './github'; import * as results from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; /** - * Extracts teams and users out of a GitHub org. + * Extracts repositories out of a GitHub org. */ export class GithubDiscoveryProcessor implements CatalogProcessor { - private readonly providers: ProviderConfig[]; + private readonly gitHubConfigMap: Map; private readonly logger: Logger; static fromConfig(config: Config, options: { logger: Logger }) { + const configs = readGitHubIntegrationConfigs( + config.getOptionalConfigArray('integrations.github') ?? [], + ); + const gitHubConfigMap = new Map(configs.map(c => [c.host, c])); + return new GithubDiscoveryProcessor({ ...options, - providers: readGithubConfig(config), + gitHubConfigMap, }); } - constructor(options: { providers: ProviderConfig[]; logger: Logger }) { - this.providers = options.providers; + constructor(options: { + gitHubConfigMap: Map; + logger: Logger; + }) { + this.gitHubConfigMap = options.gitHubConfigMap; this.logger = options.logger; } @@ -54,24 +63,23 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { return false; } - const provider = this.providers.find(p => - location.target.startsWith(`${p.target}/`), + const gitHubConfig = this.gitHubConfigMap.get( + new URL(location.target).hostname, ); - if (!provider) { + if (!gitHubConfig) { throw new Error( - `There is no GitHub Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.githubOrg.providers.`, + `There is no GitHub integration that matches ${location.target}. Please add a configuration entry for it under integrations.github`, ); } - + const { headers } = await GithubCredentialsProvider.create( + gitHubConfig, + ).getCredentials({ url: location.target }); const { org, repoSearchPath, catalogPath } = parseUrl(location.target); - const client = !provider.token - ? graphql - : graphql.defaults({ - baseUrl: provider.apiBaseUrl, - headers: { - authorization: `token ${provider.token}`, - }, - }); + + const client = graphql.defaults({ + baseUrl: gitHubConfig.apiBaseUrl, + headers, + }); // Read out all of the raw data const startTimestamp = Date.now(); diff --git a/yarn.lock b/yarn.lock index 7c743bc5b3..810090a45f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2473,9 +2473,11 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.7.0" + version "0.2.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" + integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2484,9 +2486,11 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.7.0" + version "0.3.1" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" + integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2495,16 +2499,17 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.5.0" + version "0.3.2" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" + integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== dependencies: - "@backstage/config" "^0.1.2" - "@backstage/core-api" "^0.2.8" - "@backstage/theme" "^0.2.2" + "@backstage/config" "^0.1.1" + "@backstage/core-api" "^0.2.1" + "@backstage/theme" "^0.2.1" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" @@ -2513,7 +2518,7 @@ d3-shape "^2.0.0" d3-zoom "^2.0.0" dagre "^0.8.5" - immer "^8.0.1" + immer "^7.0.9" lodash "^4.17.15" material-table "^1.69.1" prop-types "^15.7.2" @@ -2532,6 +2537,18 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/integration@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@backstage/integration/-/integration-0.3.1.tgz#4bdf1883b7149f7d4866bf1732d8e371f37ed143" + integrity sha512-aHfZ83z3XmO3R1SPG3r3W9KjSab/aFJ+51LjvRr6OCthfFTIusEmFq4cro5RUn7IPTn/0L8qpBSW2xH6FlRuVw== + dependencies: + "@backstage/config" "^0.1.2" + "@octokit/auth-app" "^2.10.5" + "@octokit/rest" "^18.0.12" + cross-fetch "^3.0.6" + git-url-parse "^11.4.4" + luxon "^1.25.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -14180,6 +14197,13 @@ git-url-parse@^11.4.3: dependencies: git-up "^4.0.0" +git-url-parse@^11.4.4: + version "11.4.4" + resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" + integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw== + dependencies: + git-up "^4.0.0" + gitconfiglocal@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" @@ -15309,6 +15333,11 @@ immer@1.10.0: resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immer@^7.0.9: + version "7.0.15" + resolved "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz#dc3bc6db87401659d2e737c67a21b227c484a4ad" + integrity sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA== + immer@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" From 6ed2b47d60dbe303edb71d6ee8b186005eb2d25e Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 5 Jan 2021 11:15:13 +0100 Subject: [PATCH 005/187] Add identity token to api requests --- .changeset/bright-rules-know.md | 12 ++ .../catalog-client/src/CatalogClient.test.ts | 18 ++- packages/catalog-client/src/CatalogClient.ts | 28 ++++- packages/catalog-client/src/types.ts | 14 +++ .../lib/catalog/CatalogIdentityClient.test.ts | 114 +++++++++++++++--- .../src/lib/catalog/CatalogIdentityClient.ts | 55 +++++++-- .../src/providers/google/provider.ts | 27 ++++- plugins/auth-backend/src/providers/types.ts | 2 - plugins/auth-backend/src/service/router.ts | 3 - .../src/api/CatalogImportClient.ts | 14 ++- plugins/catalog-import/src/plugin.ts | 11 +- plugins/catalog/src/plugin.ts | 6 +- plugins/fossa/src/api/FossaClient.ts | 14 ++- plugins/fossa/src/plugin.ts | 10 +- .../src/api/KubernetesBackendClient.ts | 11 +- plugins/kubernetes/src/plugin.ts | 7 +- plugins/rollbar/src/api/RollbarClient.ts | 16 ++- plugins/rollbar/src/plugin.ts | 6 +- .../src/lib/catalog/CatalogEntityClient.ts | 10 +- .../scaffolder-backend/src/service/router.ts | 7 +- 20 files changed, 317 insertions(+), 68 deletions(-) create mode 100644 .changeset/bright-rules-know.md diff --git a/.changeset/bright-rules-know.md b/.changeset/bright-rules-know.md new file mode 100644 index 0000000000..bc837a0bae --- /dev/null +++ b/.changeset/bright-rules-know.md @@ -0,0 +1,12 @@ +--- +'@backstage/catalog-client': minor +'@backstage/plugin-auth-backend': minor +'@backstage/plugin-catalog': minor +'@backstage/plugin-catalog-import': minor +'@backstage/plugin-fossa': minor +'@backstage/plugin-kubernetes': minor +'@backstage/plugin-rollbar': minor +'@backstage/plugin-scaffolder-backend': minor +--- + +Add identity token to api requests diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 6369f95b76..9ee4fe3165 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { CatalogClient } from './CatalogClient'; -import { CatalogListResponse, DiscoveryApi } from './types'; +import { CatalogListResponse, DiscoveryApi, IdentityApi } from './types'; const server = setupServer(); const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; @@ -27,6 +27,20 @@ const discoveryApi: DiscoveryApi = { return mockBaseUrl; }, }; +const identityApi: IdentityApi = { + getUserId() { + return 'jane-fonda'; + }, + getProfile() { + return { email: 'jane-fonda@spotify.com' }; + }, + async getIdToken() { + return Promise.resolve('fake-id-token'); + }, + async signOut() { + return Promise.resolve(); + }, +}; describe('CatalogClient', () => { let client: CatalogClient; @@ -36,7 +50,7 @@ describe('CatalogClient', () => { afterEach(() => server.resetHandlers()); beforeEach(() => { - client = new CatalogClient({ discoveryApi }); + client = new CatalogClient({ discoveryApi, identityApi }); }); describe('getEntities', () => { diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 362b1e71da..c559cb1210 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -28,13 +28,19 @@ import { CatalogEntitiesRequest, CatalogListResponse, DiscoveryApi, + IdentityApi, } from './types'; export class CatalogClient implements CatalogApi { 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 getLocationById(id: String): Promise { @@ -76,12 +82,14 @@ export class CatalogClient implements CatalogApi { target, dryRun, }: AddLocationRequest): Promise { + const idToken = await this.identityApi.getIdToken(); const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ dryRun ? '?dryRun=true' : '' }`, { headers: { + authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json', }, method: 'POST', @@ -119,9 +127,13 @@ export class CatalogClient implements CatalogApi { } async removeEntityByUid(uid: string): Promise { + const idToken = await this.identityApi.getIdToken(); const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`, { + headers: { + authorization: `Bearer ${idToken}`, + }, method: 'DELETE', }, ); @@ -140,7 +152,12 @@ export class CatalogClient implements CatalogApi { private async getRequired(path: string): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const response = await fetch(url); + const idToken = await this.identityApi.getIdToken(); + const response = await fetch(url, { + headers: { + authorization: `Bearer ${idToken}`, + }, + }); if (!response.ok) { const payload = await response.text(); @@ -153,7 +170,12 @@ export class CatalogClient implements CatalogApi { private async getOptional(path: string): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const response = await fetch(url); + const idToken = await this.identityApi.getIdToken(); + const response = await fetch(url, { + headers: { + authorization: `Bearer ${idToken}`, + }, + }); if (!response.ok) { if (response.status === 404) { diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index e72317d444..0742dfed53 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -53,3 +53,17 @@ export type AddLocationResponse = { export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; }; +/** + * This is a copy of the core IdentityApi, to avoid importing core. + */ +export type ProfileInfo = { + email?: string; + displayName?: string; + picture?: string; +}; +export type IdentityApi = { + getUserId(): string; + getProfile(): ProfileInfo; + getIdToken(): Promise; + signOut(): Promise; +}; diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index ec6aa1b6ba..0594785760 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -14,35 +14,109 @@ * limitations under the License. */ -import { CatalogApi } from '@backstage/catalog-client'; import { UserEntity } from '@backstage/catalog-model'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; import { CatalogIdentityClient } from './CatalogIdentityClient'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +const server = setupServer(); +const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; +const discovery: PluginEndpointDiscovery = { + async getBaseUrl(_pluginId) { + return mockBaseUrl; + }, + async getExternalBaseUrl(_pluginId) { + return mockBaseUrl; + }, +}; describe('CatalogIdentityClient', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - }; + let client: CatalogIdentityClient; - afterEach(() => jest.resetAllMocks()); + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); + afterAll(() => server.close()); + afterEach(() => server.resetHandlers()); - it('passes through the correct search params', async () => { - catalogApi.getEntities.mockResolvedValueOnce({ items: [{} as UserEntity] }); - const client = new CatalogIdentityClient({ - catalogApi: catalogApi as CatalogApi, + beforeEach(() => { + client = new CatalogIdentityClient({ discovery }); + }); + + describe('findUser', () => { + const defaultServiceResponse: UserEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'Test1', + namespace: 'test1', + annotations: { + key: 'value', + }, + }, + spec: { + memberOf: ['group1'], + }, + }, + ]; + + beforeEach(() => { + server.use( + rest.get(`${mockBaseUrl}/entities`, (_, res, ctx) => { + return res(ctx.json(defaultServiceResponse)); + }), + ); }); - client.findUser({ annotations: { key: 'value' } }); + it('should entities from correct endpoint', async () => { + const response = await client.findUser({ annotations: { key: 'value' } }); + expect(response).toEqual(defaultServiceResponse[0]); + }); - expect(catalogApi.getEntities).toBeCalledWith({ - filter: { - kind: 'user', - 'metadata.annotations.key': 'value', - }, + it('builds entity search filters properly', async () => { + expect.assertions(2); + + server.use( + rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { + expect(req.url.search).toBe( + '?filter=kind=user,metadata.annotations.key=value', + ); + return res(ctx.json(defaultServiceResponse)); + }), + ); + + const response = await client.findUser({ annotations: { key: 'value' } }); + + expect(response).toEqual(defaultServiceResponse[0]); + }); + + it('omits authorization header if not available', async () => { + expect.assertions(1); + + server.use( + rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { + expect(req.headers.has('authorization')).toBe(false); + return res(ctx.json([])); + }), + ); + + client.findUser({ annotations: { key: 'value' } }); + }); + + it('adds authorization header if available', async () => { + expect.assertions(1); + + server.use( + rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { + expect(req.headers.get('authorization')).toEqual('hello'); + return res(ctx.json([])); + }), + ); + + client.findUser( + { annotations: { key: 'value' } }, + { headers: { authorization: 'hello' } }, + ); }); }); }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 5bc4e5de21..b746d59cad 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -import { ConflictError, NotFoundError } from '@backstage/backend-common'; -import { CatalogApi } from '@backstage/catalog-client'; +import fetch from 'cross-fetch'; +import { + ConflictError, + NotFoundError, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; import { UserEntity } from '@backstage/catalog-model'; - type UserQuery = { annotations: Record; }; @@ -26,10 +29,10 @@ type UserQuery = { * A catalog client tailored for reading out identity data from the catalog. */ export class CatalogIdentityClient { - private readonly catalogApi: CatalogApi; + private readonly discovery: PluginEndpointDiscovery; - constructor(options: { catalogApi: CatalogApi }) { - this.catalogApi = options.catalogApi; + constructor(options: { discovery: PluginEndpointDiscovery }) { + this.discovery = options.discovery; } /** @@ -37,24 +40,54 @@ export class CatalogIdentityClient { * * Throws a NotFoundError or ConflictError if 0 or multiple users are found. */ - async findUser(query: UserQuery): Promise { + async findUser( + query: UserQuery, + options?: { headers?: Record }, + ): Promise { const filter: Record = { kind: 'user', }; for (const [key, value] of Object.entries(query.annotations)) { filter[`metadata.annotations.${key}`] = value; } + const params: string[] = []; - const { items } = await this.catalogApi.getEntities({ filter }); + const filterParts: string[] = []; + for (const [key, value] of Object.entries(filter)) { + for (const v of [value].flat()) { + filterParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`); + } + } + if (filterParts.length) { + params.push(`filter=${filterParts.join(',')}`); + } + const queryPart = params.length ? `?${params.join('&')}` : ''; - if (items.length !== 1) { - if (items.length > 1) { + const url = `${await this.discovery.getBaseUrl( + 'catalog', + )}/entities${queryPart}`; + const response = await fetch(url, { + headers: { + ...options?.headers, + }, + }); + + if (!response.ok) { + const payload = await response.text(); + const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`; + throw new Error(message); + } + + const entities: UserEntity[] = await response.json(); + + if (entities.length !== 1) { + if (entities.length > 1) { throw new ConflictError('User lookup resulted in multiple matches'); } else { throw new NotFoundError('User not found'); } } - return items[0] as UserEntity; + return entities[0] as UserEntity; } } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index ae4f5fabd0..10f3ae96f2 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -38,6 +38,7 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { AuthProviderFactory, RedirectInfo } from '../types'; +import { TokenIssuer } from '../../identity'; type PrivateInfo = { refreshToken: string; @@ -46,16 +47,19 @@ type PrivateInfo = { export type GoogleAuthProviderOptions = OAuthProviderOptions & { logger: Logger; identityClient: CatalogIdentityClient; + tokenIssuer: TokenIssuer; }; export class GoogleAuthProvider implements OAuthHandlers { private readonly _strategy: GoogleStrategy; private readonly logger: Logger; private readonly identityClient: CatalogIdentityClient; + private readonly tokenIssuer: TokenIssuer; constructor(options: GoogleAuthProviderOptions) { this.logger = options.logger; this.identityClient = options.identityClient; + this.tokenIssuer = options.tokenIssuer; // TODO: throw error if env variables not set? this._strategy = new GoogleStrategy( { @@ -150,11 +154,21 @@ export class GoogleAuthProvider implements OAuthHandlers { } try { - const user = await this.identityClient.findUser({ - annotations: { - 'google.com/email': profile.email, - }, + const token = await this.tokenIssuer.issueToken({ + claims: { sub: 'backstage.io/auth-backend' }, }); + const user = await this.identityClient.findUser( + { + annotations: { + 'google.com/email': profile.email, + }, + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + }, + ); return { ...response, @@ -180,7 +194,7 @@ export const createGoogleProvider: AuthProviderFactory = ({ config, logger, tokenIssuer, - catalogApi, + discovery, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); @@ -192,7 +206,8 @@ export const createGoogleProvider: AuthProviderFactory = ({ clientSecret, callbackUrl, logger, - identityClient: new CatalogIdentityClient({ catalogApi }), + tokenIssuer, + identityClient: new CatalogIdentityClient({ discovery }), }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 1a4e7b118a..8339258654 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -15,7 +15,6 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -132,7 +131,6 @@ export type AuthProviderFactoryOptions = { logger: Logger; tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; - catalogApi: CatalogApi; identityResolver?: ExperimentalIdentityResolver; }; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 85d60661e9..742a269b73 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -27,7 +27,6 @@ import { PluginDatabaseManager, PluginEndpointDiscovery, } from '@backstage/backend-common'; -import { CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity'; import session from 'express-session'; @@ -66,7 +65,6 @@ export async function createRouter({ keyDurationSeconds, logger: logger.child({ component: 'token-factory' }), }); - const catalogApi = new CatalogClient({ discoveryApi: discovery }); const secret = config.getOptionalString('auth.session.secret'); if (secret) { @@ -103,7 +101,6 @@ export async function createRouter({ logger, tokenIssuer, discovery, - catalogApi, }); const r = Router(); diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index d1509c2105..9b52de0ba4 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -15,23 +15,31 @@ */ import { Octokit } from '@octokit/rest'; -import { DiscoveryApi, OAuthApi, ConfigApi } from '@backstage/core'; +import { + DiscoveryApi, + IdentityApi, + OAuthApi, + ConfigApi, +} from '@backstage/core'; import { CatalogImportApi } from './CatalogImportApi'; import { PartialEntity } from '../util/types'; import { GitHubIntegrationConfig } from '@backstage/integration'; export class CatalogImportClient implements CatalogImportApi { private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; private readonly githubAuthApi: OAuthApi; private readonly configApi: ConfigApi; constructor(options: { discoveryApi: DiscoveryApi; githubAuthApi: OAuthApi; + identityApi: IdentityApi; configApi: ConfigApi; }) { this.discoveryApi = options.discoveryApi; this.githubAuthApi = options.githubAuthApi; + this.identityApi = options.identityApi; this.configApi = options.configApi; } @@ -40,10 +48,12 @@ export class CatalogImportClient implements CatalogImportApi { }: { repo: string; }): Promise { + const idToken = await this.identityApi.getIdToken(); const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`, { headers: { + authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json', }, method: 'POST', @@ -69,10 +79,12 @@ export class CatalogImportClient implements CatalogImportApi { }: { location: string; }): Promise { + const idToken = await this.identityApi.getIdToken(); const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, { headers: { + authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json', }, method: 'POST', diff --git a/plugins/catalog-import/src/plugin.ts b/plugins/catalog-import/src/plugin.ts index 42210d0ed1..60e5128df0 100644 --- a/plugins/catalog-import/src/plugin.ts +++ b/plugins/catalog-import/src/plugin.ts @@ -20,6 +20,7 @@ import { createRouteRef, discoveryApiRef, githubAuthApiRef, + identityApiRef, configApiRef, createRoutableExtension, } from '@backstage/core'; @@ -39,10 +40,16 @@ export const catalogImportPlugin = createPlugin({ deps: { discoveryApi: discoveryApiRef, githubAuthApi: githubAuthApiRef, + identityApi: identityApiRef, configApi: configApiRef, }, - factory: ({ discoveryApi, githubAuthApi, configApi }) => - new CatalogImportClient({ discoveryApi, githubAuthApi, configApi }), + factory: ({ discoveryApi, githubAuthApi, identityApi, configApi }) => + new CatalogImportClient({ + discoveryApi, + githubAuthApi, + identityApi, + configApi, + }), }), ], routes: { diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index c19925775d..557762ee46 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -20,6 +20,7 @@ import { createPlugin, discoveryApiRef, createRoutableExtension, + identityApiRef, } from '@backstage/core'; import { catalogApiRef, @@ -32,8 +33,9 @@ export const catalogPlugin = createPlugin({ apis: [ createApiFactory({ api: catalogApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new CatalogClient({ discoveryApi, identityApi }), }), ], routes: { diff --git a/plugins/fossa/src/api/FossaClient.ts b/plugins/fossa/src/api/FossaClient.ts index 05a889177a..6730f7ddb3 100644 --- a/plugins/fossa/src/api/FossaClient.ts +++ b/plugins/fossa/src/api/FossaClient.ts @@ -14,28 +14,38 @@ * limitations under the License. */ -import { DiscoveryApi } from '@backstage/core'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; import fetch from 'cross-fetch'; import { FindingSummary, FossaApi } from './FossaApi'; export class FossaClient implements FossaApi { discoveryApi: DiscoveryApi; + identityApi?: IdentityApi; organizationId?: string; constructor({ discoveryApi, + identityApi, organizationId, }: { discoveryApi: DiscoveryApi; + identityApi?: IdentityApi; organizationId?: string; }) { this.discoveryApi = discoveryApi; + this.identityApi = identityApi; this.organizationId = organizationId; } private async callApi(path: string): Promise { const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`; - const response = await fetch(`${apiUrl}/${path}`); + const headers: Record = {}; + if (this.identityApi) { + headers.authorization = `Bearer ${this.identityApi.getIdToken()}`; + } + const response = await fetch(`${apiUrl}/${path}`, { + headers, + }); if (response.status === 200) { return await response.json(); } diff --git a/plugins/fossa/src/plugin.ts b/plugins/fossa/src/plugin.ts index cb1de3911f..638be4a0bf 100644 --- a/plugins/fossa/src/plugin.ts +++ b/plugins/fossa/src/plugin.ts @@ -19,6 +19,7 @@ import { createApiFactory, createPlugin, discoveryApiRef, + identityApiRef, } from '@backstage/core'; import { fossaApiRef, FossaClient } from './api'; @@ -27,10 +28,15 @@ export const fossaPlugin = createPlugin({ apis: [ createApiFactory({ api: fossaApiRef, - deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, - factory: ({ configApi, discoveryApi }) => + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ configApi, discoveryApi, identityApi }) => new FossaClient({ discoveryApi, + identityApi, organizationId: configApi.getOptionalString('fossa.organizationId'), }), }), diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index 287b411259..e005b29517 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DiscoveryApi } from '@backstage/core'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; import { KubernetesApi } from './types'; import { KubernetesRequestBody, @@ -23,9 +23,14 @@ import { export class KubernetesBackendClient implements KubernetesApi { 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; } private async getRequired( @@ -33,9 +38,11 @@ export class KubernetesBackendClient implements KubernetesApi { requestBody: KubernetesRequestBody, ): Promise { const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`; + const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { method: 'POST', headers: { + authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index cff9e1468b..04ab7a086f 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -18,6 +18,7 @@ import { createPlugin, createRouteRef, discoveryApiRef, + identityApiRef, googleAuthApiRef, } from '@backstage/core'; import { KubernetesBackendClient } from './api/KubernetesBackendClient'; @@ -35,9 +36,9 @@ export const plugin = createPlugin({ apis: [ createApiFactory({ api: kubernetesApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => - new KubernetesBackendClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new KubernetesBackendClient({ discoveryApi, identityApi }), }), createApiFactory({ api: kubernetesAuthProvidersApiRef, diff --git a/plugins/rollbar/src/api/RollbarClient.ts b/plugins/rollbar/src/api/RollbarClient.ts index e612890655..1041accf19 100644 --- a/plugins/rollbar/src/api/RollbarClient.ts +++ b/plugins/rollbar/src/api/RollbarClient.ts @@ -20,13 +20,18 @@ import { RollbarProject, RollbarTopActiveItem, } from './types'; -import { DiscoveryApi } from '@backstage/core'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; export class RollbarClient implements RollbarApi { 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 getAllProjects(): Promise { @@ -53,7 +58,12 @@ export class RollbarClient implements RollbarApi { private async get(path: string): Promise { const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`; - const response = await fetch(url); + const idToken = await this.identityApi.getIdToken(); + const response = await fetch(url, { + headers: { + authorization: `Bearer ${idToken}`, + }, + }); if (!response.ok) { const payload = await response.text(); diff --git a/plugins/rollbar/src/plugin.ts b/plugins/rollbar/src/plugin.ts index c3ca6173ff..9456c10c6b 100644 --- a/plugins/rollbar/src/plugin.ts +++ b/plugins/rollbar/src/plugin.ts @@ -18,6 +18,7 @@ import { createPlugin, createApiFactory, discoveryApiRef, + identityApiRef, } from '@backstage/core'; import { rootRouteRef, entityRouteRef } from './routes'; import { RollbarHome } from './components/RollbarHome/RollbarHome'; @@ -30,8 +31,9 @@ export const plugin = createPlugin({ apis: [ createApiFactory({ api: rollbarApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new RollbarClient({ discoveryApi, identityApi }), }), ], register({ router }) { diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 4541f03a4a..c5e20202c5 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -37,7 +37,10 @@ export class CatalogEntityClient { * * Throws a NotFoundError or ConflictError if 0 or multiple templates are found. */ - async findTemplate(templateName: string): Promise { + async findTemplate( + templateName: string, + options?: { headers?: Record }, + ): Promise { const conditions = [ 'kind=template', `metadata.name=${encodeURIComponent(templateName)}`, @@ -46,6 +49,11 @@ export class CatalogEntityClient { const baseUrl = await this.discovery.getBaseUrl('catalog'); const response = await fetch( `${baseUrl}/entities?filter=${conditions.join(',')}`, + { + headers: { + ...options?.headers, + }, + }, ); if (!response.ok) { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9098e5d05e..070b7cffc7 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -97,7 +97,12 @@ export async function createRouter( }, }; - const template = await entityClient.findTemplate(templateName); + // Forward authorization header from client + const template = await entityClient.findTemplate(templateName, { + headers: req.headers.authorization + ? { authorization: req.headers.authorization } + : {}, + }); const validationResult: ValidatorResult = validate( values, From 570884ece6afd2843090422ad1d452e0df1dbdac Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 5 Jan 2021 11:22:50 +0100 Subject: [PATCH 006/187] Fix linting --- .../src/lib/catalog/CatalogIdentityClient.test.ts | 4 ++-- plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 0594785760..9d29e68277 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -23,10 +23,10 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; const server = setupServer(); const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; const discovery: PluginEndpointDiscovery = { - async getBaseUrl(_pluginId) { + async getBaseUrl() { return mockBaseUrl; }, - async getExternalBaseUrl(_pluginId) { + async getExternalBaseUrl() { return mockBaseUrl; }, }; diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index b746d59cad..f4aa722b2d 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -21,6 +21,7 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { UserEntity } from '@backstage/catalog-model'; + type UserQuery = { annotations: Record; }; From b52baa518b4868a3c344a752cfa29fab1c24515a Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 19 Jan 2021 18:34:21 +0100 Subject: [PATCH 007/187] Make identityApi required --- plugins/fossa/src/api/FossaClient.test.ts | 25 ++++++++++++++++++++--- plugins/fossa/src/api/FossaClient.ts | 11 +++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/plugins/fossa/src/api/FossaClient.test.ts b/plugins/fossa/src/api/FossaClient.test.ts index dd0fdeff5e..820210e2b6 100644 --- a/plugins/fossa/src/api/FossaClient.test.ts +++ b/plugins/fossa/src/api/FossaClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlPatternDiscovery } from '@backstage/core'; +import { UrlPatternDiscovery, IdentityApi } from '@backstage/core'; import { msw } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -22,6 +22,21 @@ import { FindingSummary, FossaApi, FossaClient } from './index'; const server = setupServer(); +const identityApi: IdentityApi = { + getUserId() { + return 'jane-fonda'; + }, + getProfile() { + return { email: 'jane-fonda@spotify.com' }; + }, + async getIdToken() { + return Promise.resolve('fake-id-token'); + }, + async signOut() { + return Promise.resolve(); + }, +}; + describe('FossaClient', () => { msw.setupDefaultHandlers(server); @@ -30,7 +45,11 @@ describe('FossaClient', () => { let client: FossaApi; beforeEach(() => { - client = new FossaClient({ discoveryApi, organizationId: '8736' }); + client = new FossaClient({ + discoveryApi, + identityApi, + organizationId: '8736', + }); }); it('should report finding summary', async () => { @@ -137,7 +156,7 @@ describe('FossaClient', () => { }); it('should skip organizationId', async () => { - client = new FossaClient({ discoveryApi }); + client = new FossaClient({ discoveryApi, identityApi }); server.use( rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { diff --git a/plugins/fossa/src/api/FossaClient.ts b/plugins/fossa/src/api/FossaClient.ts index 6730f7ddb3..33c8c65c5f 100644 --- a/plugins/fossa/src/api/FossaClient.ts +++ b/plugins/fossa/src/api/FossaClient.ts @@ -20,7 +20,7 @@ import { FindingSummary, FossaApi } from './FossaApi'; export class FossaClient implements FossaApi { discoveryApi: DiscoveryApi; - identityApi?: IdentityApi; + identityApi: IdentityApi; organizationId?: string; constructor({ @@ -29,7 +29,7 @@ export class FossaClient implements FossaApi { organizationId, }: { discoveryApi: DiscoveryApi; - identityApi?: IdentityApi; + identityApi: IdentityApi; organizationId?: string; }) { this.discoveryApi = discoveryApi; @@ -39,10 +39,9 @@ export class FossaClient implements FossaApi { private async callApi(path: string): Promise { const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`; - const headers: Record = {}; - if (this.identityApi) { - headers.authorization = `Bearer ${this.identityApi.getIdToken()}`; - } + const headers: Record = { + authorization: await `Bearer ${this.identityApi.getIdToken()}`, + }; const response = await fetch(`${apiUrl}/${path}`, { headers, }); From 3647d0577f7e18201e98f2de24c6762e165b9e47 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 19 Jan 2021 18:38:33 +0100 Subject: [PATCH 008/187] Add token to CatalogClient and add wrapper class for frontend --- .../catalog-client/src/CatalogClient.test.ts | 25 ++---- packages/catalog-client/src/CatalogClient.ts | 84 +++++++++++-------- packages/catalog-client/src/index.ts | 8 +- packages/catalog-client/src/types.ts | 14 ---- .../catalog/src/CatalogClientWrapper.test.ts | 65 ++++++++++++++ plugins/catalog/src/CatalogClientWrapper.ts | 68 +++++++++++++++ plugins/catalog/src/plugin.ts | 7 +- 7 files changed, 200 insertions(+), 71 deletions(-) create mode 100644 plugins/catalog/src/CatalogClientWrapper.test.ts create mode 100644 plugins/catalog/src/CatalogClientWrapper.ts diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 9ee4fe3165..20cc35f423 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -18,29 +18,16 @@ import { Entity } from '@backstage/catalog-model'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { CatalogClient } from './CatalogClient'; -import { CatalogListResponse, DiscoveryApi, IdentityApi } from './types'; +import { CatalogListResponse, DiscoveryApi } from './types'; const server = setupServer(); +const token = 'fake-token'; const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; const discoveryApi: DiscoveryApi = { async getBaseUrl(_pluginId) { return mockBaseUrl; }, }; -const identityApi: IdentityApi = { - getUserId() { - return 'jane-fonda'; - }, - getProfile() { - return { email: 'jane-fonda@spotify.com' }; - }, - async getIdToken() { - return Promise.resolve('fake-id-token'); - }, - async signOut() { - return Promise.resolve(); - }, -}; describe('CatalogClient', () => { let client: CatalogClient; @@ -50,7 +37,7 @@ describe('CatalogClient', () => { afterEach(() => server.resetHandlers()); beforeEach(() => { - client = new CatalogClient({ discoveryApi, identityApi }); + client = new CatalogClient({ discoveryApi }); }); describe('getEntities', () => { @@ -85,7 +72,7 @@ describe('CatalogClient', () => { }); it('should entities from correct endpoint', async () => { - const response = await client.getEntities(); + const response = await client.getEntities(token); expect(response).toEqual(defaultResponse); }); @@ -99,7 +86,7 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities({ + const response = await client.getEntities(token, { filter: { a: '1', b: ['2', '3'], @@ -120,7 +107,7 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities({ + const response = await client.getEntities(token, { fields: ['a.b', 'ö'], }); diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index c559cb1210..c4f8c3d2b0 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -24,30 +24,27 @@ import fetch from 'cross-fetch'; import { AddLocationRequest, AddLocationResponse, - CatalogApi, CatalogEntitiesRequest, CatalogListResponse, DiscoveryApi, - IdentityApi, } from './types'; -export class CatalogClient implements CatalogApi { +export class CatalogClient { private readonly discoveryApi: DiscoveryApi; - private readonly identityApi: IdentityApi; - constructor(options: { - discoveryApi: DiscoveryApi; - identityApi: IdentityApi; - }) { + constructor(options: { discoveryApi: DiscoveryApi }) { this.discoveryApi = options.discoveryApi; - this.identityApi = options.identityApi; } - async getLocationById(id: String): Promise { - return await this.getOptional(`/locations/${id}`); + async getLocationById( + token: string | undefined, + id: String, + ): Promise { + return await this.getOptional(token, `/locations/${id}`); } async getEntities( + token: string | undefined, request?: CatalogEntitiesRequest, ): Promise> { const { filter = {}, fields = [] } = request ?? {}; @@ -68,28 +65,35 @@ export class CatalogClient implements CatalogApi { } const query = params.length ? `?${params.join('&')}` : ''; - const entities: Entity[] = await this.getRequired(`/entities${query}`); + const entities: Entity[] = await this.getRequired( + token, + `/entities${query}`, + ); return { items: entities }; } - async getEntityByName(compoundName: EntityName): Promise { + async getEntityByName( + token: string | undefined, + compoundName: EntityName, + ): Promise { const { kind, namespace = 'default', name } = compoundName; - return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`); + return this.getOptional( + token, + `/entities/by-name/${kind}/${namespace}/${name}`, + ); } - async addLocation({ - type = 'url', - target, - dryRun, - }: AddLocationRequest): Promise { - const idToken = await this.identityApi.getIdToken(); + async addLocation( + token: string | undefined, + { type = 'url', target, dryRun }: AddLocationRequest, + ): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ dryRun ? '?dryRun=true' : '' }`, { headers: { - authorization: `Bearer ${idToken}`, + authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, method: 'POST', @@ -118,21 +122,29 @@ export class CatalogClient implements CatalogApi { }; } - async getLocationByEntity(entity: Entity): Promise { + async getLocationByEntity( + token: string | undefined, + entity: Entity, + ): Promise { const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION]; - const all: { data: Location }[] = await this.getRequired('/locations'); + const all: { data: Location }[] = await this.getRequired( + token, + '/locations', + ); return all .map(r => r.data) .find(l => locationCompound === `${l.type}:${l.target}`); } - async removeEntityByUid(uid: string): Promise { - const idToken = await this.identityApi.getIdToken(); + async removeEntityByUid( + token: string | undefined, + uid: string, + ): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`, { headers: { - authorization: `Bearer ${idToken}`, + authorization: `Bearer ${token}`, }, method: 'DELETE', }, @@ -150,13 +162,13 @@ export class CatalogClient implements CatalogApi { // Private methods // - private async getRequired(path: string): Promise { + private async getRequired( + token: string | undefined, + path: string, + ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { - headers: { - authorization: `Bearer ${idToken}`, - }, + headers: token ? { authorization: `Bearer ${token}` } : {}, }); if (!response.ok) { @@ -168,13 +180,13 @@ export class CatalogClient implements CatalogApi { return await response.json(); } - private async getOptional(path: string): Promise { + private async getOptional( + token: string | undefined, + path: string, + ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { - headers: { - authorization: `Bearer ${idToken}`, - }, + headers: token ? { authorization: `Bearer ${token}` } : {}, }); if (!response.ok) { diff --git a/packages/catalog-client/src/index.ts b/packages/catalog-client/src/index.ts index 9bd13f9b7c..c5a626e25b 100644 --- a/packages/catalog-client/src/index.ts +++ b/packages/catalog-client/src/index.ts @@ -15,4 +15,10 @@ */ export { CatalogClient } from './CatalogClient'; -export type { CatalogApi } from './types'; +export type { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogEntitiesRequest, + CatalogListResponse, +} from './types'; diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index 0742dfed53..e72317d444 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -53,17 +53,3 @@ export type AddLocationResponse = { export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; }; -/** - * This is a copy of the core IdentityApi, to avoid importing core. - */ -export type ProfileInfo = { - email?: string; - displayName?: string; - picture?: string; -}; -export type IdentityApi = { - getUserId(): string; - getProfile(): ProfileInfo; - getIdToken(): Promise; - signOut(): Promise; -}; diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts new file mode 100644 index 0000000000..8a5c58194a --- /dev/null +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { CatalogClient } from '@backstage/catalog-client'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; +import { CatalogClientWrapper } from './CatalogClientWrapper'; + +jest.mock('./CatalogClient'); +const MockedCatalogClient = CatalogClient as jest.Mock; + +const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; +const discoveryApi: DiscoveryApi = { + async getBaseUrl(_pluginId) { + return mockBaseUrl; + }, +}; +const identityApi: IdentityApi = { + getUserId() { + return 'jane-fonda'; + }, + getProfile() { + return { email: 'jane-fonda@spotify.com' }; + }, + async getIdToken() { + return Promise.resolve('fake-id-token'); + }, + async signOut() { + return Promise.resolve(); + }, +}; + +describe('CatalogClientWrapper', () => { + let client: CatalogClientWrapper; + + beforeEach(() => { + MockedCatalogClient.mockClear(); + client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + }); + + describe('getEntities', () => { + it('injects authorization token', async () => { + expect.assertions(2); + await client.getEntities(); + const getEntities = MockedCatalogClient.mock.instances[0].getEntities; + expect(getEntities).toHaveBeenCalledWith('fake-id-token', undefined); + expect(getEntities).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts new file mode 100644 index 0000000000..fab551de48 --- /dev/null +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, EntityName, Location } from '@backstage/catalog-model'; +import { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogEntitiesRequest, + CatalogListResponse, + CatalogClient, +} from '@backstage/catalog-client'; +import { IdentityApi } from '@backstage/core'; + +export class CatalogClientWrapper implements CatalogApi { + private readonly identityApi: IdentityApi; + private readonly client: CatalogClient; + + constructor(options: { client: CatalogClient; identityApi: IdentityApi }) { + this.client = options.client; + this.identityApi = options.identityApi; + } + + async getLocationById(id: String): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.getLocationById(token, id); + } + + async getEntities( + request?: CatalogEntitiesRequest, + ): Promise> { + const token = await this.identityApi.getIdToken(); + return await this.client.getEntities(token, request); + } + + async getEntityByName(compoundName: EntityName): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.getEntityByName(token, compoundName); + } + + async addLocation(request: AddLocationRequest): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.addLocation(token, request); + } + + async getLocationByEntity(entity: Entity): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.getLocationByEntity(token, entity); + } + + async removeEntityByUid(uid: string): Promise { + const token = await this.identityApi.getIdToken(); + return await this.client.removeEntityByUid(token, uid); + } +} diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 557762ee46..55a9f29372 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -27,6 +27,8 @@ import { catalogRouteRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; +import { catalogRouteRef, entityRouteRef } from './routes'; +import { CatalogClientWrapper } from './CatalogClientWrapper'; export const catalogPlugin = createPlugin({ id: 'catalog', @@ -35,7 +37,10 @@ export const catalogPlugin = createPlugin({ api: catalogApiRef, deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, factory: ({ discoveryApi, identityApi }) => - new CatalogClient({ discoveryApi, identityApi }), + new CatalogClientWrapper({ + client: new CatalogClient({ discoveryApi }), + identityApi, + }), }), ], routes: { From be2958d731f08614df02bd9213a7776e2eec7485 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 19 Jan 2021 19:17:27 +0100 Subject: [PATCH 009/187] CatalogIdentityClient uses CatalogClient --- .../lib/catalog/CatalogIdentityClient.test.ts | 112 +++--------------- .../src/lib/catalog/CatalogIdentityClient.ts | 51 ++------ .../src/providers/google/provider.ts | 19 +-- plugins/auth-backend/src/providers/types.ts | 2 + plugins/auth-backend/src/service/router.ts | 3 + 5 files changed, 38 insertions(+), 149 deletions(-) diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 9d29e68277..ffd118884e 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -14,109 +14,31 @@ * limitations under the License. */ -import { UserEntity } from '@backstage/catalog-model'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; +import { CatalogClient } from '@backstage/catalog-client'; import { CatalogIdentityClient } from './CatalogIdentityClient'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; -const server = setupServer(); -const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; -const discovery: PluginEndpointDiscovery = { - async getBaseUrl() { - return mockBaseUrl; - }, - async getExternalBaseUrl() { - return mockBaseUrl; - }, -}; +jest.mock('./CatalogClient'); +const MockedCatalogClient = CatalogClient as jest.Mock; describe('CatalogIdentityClient', () => { - let client: CatalogIdentityClient; + const token = 'fake-id-token'; - beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); - afterAll(() => server.close()); - afterEach(() => server.resetHandlers()); + afterEach(() => jest.resetAllMocks()); - beforeEach(() => { - client = new CatalogIdentityClient({ discovery }); - }); + it('passes through the correct search params', async () => { + const client = new CatalogIdentityClient({ + catalogClient: new MockedCatalogClient(), + }); - describe('findUser', () => { - const defaultServiceResponse: UserEntity[] = [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: 'Test1', - namespace: 'test1', - annotations: { - key: 'value', - }, - }, - spec: { - memberOf: ['group1'], - }, + client.findUser(token, { annotations: { key: 'value' } }); + + const getEntities = MockedCatalogClient.mock.instances[0].getEntities; + expect(getEntities).toHaveBeenCalledWith(token, { + filter: { + kind: 'user', + 'metadata.annotations.key': 'value', }, - ]; - - beforeEach(() => { - server.use( - rest.get(`${mockBaseUrl}/entities`, (_, res, ctx) => { - return res(ctx.json(defaultServiceResponse)); - }), - ); - }); - - it('should entities from correct endpoint', async () => { - const response = await client.findUser({ annotations: { key: 'value' } }); - expect(response).toEqual(defaultServiceResponse[0]); - }); - - it('builds entity search filters properly', async () => { - expect.assertions(2); - - server.use( - rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { - expect(req.url.search).toBe( - '?filter=kind=user,metadata.annotations.key=value', - ); - return res(ctx.json(defaultServiceResponse)); - }), - ); - - const response = await client.findUser({ annotations: { key: 'value' } }); - - expect(response).toEqual(defaultServiceResponse[0]); - }); - - it('omits authorization header if not available', async () => { - expect.assertions(1); - - server.use( - rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { - expect(req.headers.has('authorization')).toBe(false); - return res(ctx.json([])); - }), - ); - - client.findUser({ annotations: { key: 'value' } }); - }); - - it('adds authorization header if available', async () => { - expect.assertions(1); - - server.use( - rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { - expect(req.headers.get('authorization')).toEqual('hello'); - return res(ctx.json([])); - }), - ); - - client.findUser( - { annotations: { key: 'value' } }, - { headers: { authorization: 'hello' } }, - ); }); + expect(getEntities).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index f4aa722b2d..0fad8122f4 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -14,12 +14,8 @@ * limitations under the License. */ -import fetch from 'cross-fetch'; -import { - ConflictError, - NotFoundError, - PluginEndpointDiscovery, -} from '@backstage/backend-common'; +import { ConflictError, NotFoundError } from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; import { UserEntity } from '@backstage/catalog-model'; type UserQuery = { @@ -30,10 +26,10 @@ type UserQuery = { * A catalog client tailored for reading out identity data from the catalog. */ export class CatalogIdentityClient { - private readonly discovery: PluginEndpointDiscovery; + private readonly catalogClient: CatalogClient; - constructor(options: { discovery: PluginEndpointDiscovery }) { - this.discovery = options.discovery; + constructor(options: { catalogClient: CatalogClient }) { + this.catalogClient = options.catalogClient; } /** @@ -42,8 +38,8 @@ export class CatalogIdentityClient { * Throws a NotFoundError or ConflictError if 0 or multiple users are found. */ async findUser( + token: string | undefined, query: UserQuery, - options?: { headers?: Record }, ): Promise { const filter: Record = { kind: 'user', @@ -51,44 +47,17 @@ export class CatalogIdentityClient { for (const [key, value] of Object.entries(query.annotations)) { filter[`metadata.annotations.${key}`] = value; } - const params: string[] = []; - const filterParts: string[] = []; - for (const [key, value] of Object.entries(filter)) { - for (const v of [value].flat()) { - filterParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`); - } - } - if (filterParts.length) { - params.push(`filter=${filterParts.join(',')}`); - } - const queryPart = params.length ? `?${params.join('&')}` : ''; + const { items } = await this.catalogClient.getEntities(token, { filter }); - const url = `${await this.discovery.getBaseUrl( - 'catalog', - )}/entities${queryPart}`; - const response = await fetch(url, { - headers: { - ...options?.headers, - }, - }); - - if (!response.ok) { - const payload = await response.text(); - const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`; - throw new Error(message); - } - - const entities: UserEntity[] = await response.json(); - - if (entities.length !== 1) { - if (entities.length > 1) { + if (items.length !== 1) { + if (items.length > 1) { throw new ConflictError('User lookup resulted in multiple matches'); } else { throw new NotFoundError('User not found'); } } - return entities[0] as UserEntity; + return items[0] as UserEntity; } } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 10f3ae96f2..340ec46529 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -157,18 +157,11 @@ export class GoogleAuthProvider implements OAuthHandlers { const token = await this.tokenIssuer.issueToken({ claims: { sub: 'backstage.io/auth-backend' }, }); - const user = await this.identityClient.findUser( - { - annotations: { - 'google.com/email': profile.email, - }, + const user = await this.identityClient.findUser(token, { + annotations: { + 'google.com/email': profile.email, }, - { - headers: { - authorization: `Bearer ${token}`, - }, - }, - ); + }); return { ...response, @@ -194,7 +187,7 @@ export const createGoogleProvider: AuthProviderFactory = ({ config, logger, tokenIssuer, - discovery, + catalogClient, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); @@ -207,7 +200,7 @@ export const createGoogleProvider: AuthProviderFactory = ({ callbackUrl, logger, tokenIssuer, - identityClient: new CatalogIdentityClient({ discovery }), + identityClient: new CatalogIdentityClient({ catalogClient }), }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 8339258654..1ab90ac522 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -15,6 +15,7 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -132,6 +133,7 @@ export type AuthProviderFactoryOptions = { tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; identityResolver?: ExperimentalIdentityResolver; + catalogClient: CatalogClient; }; export type AuthProviderFactory = ( diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 742a269b73..859c2a4846 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -27,6 +27,7 @@ import { PluginDatabaseManager, PluginEndpointDiscovery, } from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity'; import session from 'express-session'; @@ -65,6 +66,7 @@ export async function createRouter({ keyDurationSeconds, logger: logger.child({ component: 'token-factory' }), }); + const catalogClient = new CatalogClient({ discoveryApi: discovery }); const secret = config.getOptionalString('auth.session.secret'); if (secret) { @@ -101,6 +103,7 @@ export async function createRouter({ logger, tokenIssuer, discovery, + catalogClient, }); const r = Router(); From 214ca40d07a0b4cc2c842a7cef5bb4d371aa6487 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 19 Jan 2021 19:46:36 +0100 Subject: [PATCH 010/187] CatalogEntityClient uses CatalogClient --- packages/backend/package.json | 1 + packages/backend/src/plugins/scaffolder.ts | 4 +- plugins/scaffolder-backend/package.json | 1 + .../src/lib/catalog/CatalogEntityClient.ts | 43 +++++-------------- .../scaffolder-backend/src/service/router.ts | 13 +++--- 5 files changed, 23 insertions(+), 39 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index aa00407952..8d215d5137 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.5.1", + "@backstage/catalog-client": "^0.3.5", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/plugin-app-backend": "^0.3.5", diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 4e2257a46c..bd3616116b 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -24,6 +24,7 @@ import { CatalogEntityClient, } from '@backstage/plugin-scaffolder-backend'; import { SingleHostDiscovery } from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; @@ -44,7 +45,8 @@ export default async function createPlugin({ const dockerClient = new Docker(); const discovery = SingleHostDiscovery.fromConfig(config); - const entityClient = new CatalogEntityClient({ discovery }); + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const entityClient = new CatalogEntityClient({ catalogClient }); return await createRouter({ preparers, diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 7af9f567e3..c9a439bea3 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.5.1", + "@backstage/catalog-client": "^0.3.4", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/integration": "^0.3.1", diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index c5e20202c5..6c2909d360 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -14,22 +14,18 @@ * limitations under the License. */ -import fetch from 'cross-fetch'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { - ConflictError, - NotFoundError, - PluginEndpointDiscovery, -} from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; +import { ConflictError, NotFoundError } from '@backstage/backend-common'; /** * A catalog client tailored for reading out entity data from the catalog. */ export class CatalogEntityClient { - private readonly discovery: PluginEndpointDiscovery; + private readonly catalogClient: CatalogClient; - constructor(options: { discovery: PluginEndpointDiscovery }) { - this.discovery = options.discovery; + constructor(options: { catalogClient: CatalogClient }) { + this.catalogClient = options.catalogClient; } /** @@ -38,32 +34,15 @@ export class CatalogEntityClient { * Throws a NotFoundError or ConflictError if 0 or multiple templates are found. */ async findTemplate( + token: string | undefined, templateName: string, - options?: { headers?: Record }, ): Promise { - const conditions = [ - 'kind=template', - `metadata.name=${encodeURIComponent(templateName)}`, - ]; - - const baseUrl = await this.discovery.getBaseUrl('catalog'); - const response = await fetch( - `${baseUrl}/entities?filter=${conditions.join(',')}`, - { - headers: { - ...options?.headers, - }, + const { items: templates } = (await this.catalogClient.getEntities(token, { + filter: { + kind: 'template', + 'metadata.name': templateName, }, - ); - - if (!response.ok) { - const text = await response.text(); - throw new Error( - `Request failed with ${response.status} ${response.statusText}, ${text}`, - ); - } - - const templates: TemplateEntityV1alpha1[] = await response.json(); + })) as { items: TemplateEntityV1alpha1[] }; if (templates.length !== 1) { if (templates.length > 1) { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 070b7cffc7..606aa05bc1 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -15,6 +15,7 @@ */ import { Config } from '@backstage/config'; +import { BackstageIdentity } from '@backstage/core'; import Docker from 'dockerode'; import express from 'express'; import { resolve as resolvePath } from 'path'; @@ -97,12 +98,12 @@ export async function createRouter( }, }; - // Forward authorization header from client - const template = await entityClient.findTemplate(templateName, { - headers: req.headers.authorization - ? { authorization: req.headers.authorization } - : {}, - }); + // Forward authorization from client + const user = req.user as BackstageIdentity; + const template = await entityClient.findTemplate( + user?.idToken, + templateName, + ); const validationResult: ValidatorResult = validate( values, From 82cac24d19e384d7795e31e9c3324d31f468956d Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 19 Jan 2021 20:51:54 +0100 Subject: [PATCH 011/187] Fix client reference --- plugins/catalog/src/CatalogClientWrapper.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index 8a5c58194a..e2aea16526 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -18,7 +18,7 @@ import { CatalogClient } from '@backstage/catalog-client'; import { DiscoveryApi, IdentityApi } from '@backstage/core'; import { CatalogClientWrapper } from './CatalogClientWrapper'; -jest.mock('./CatalogClient'); +jest.mock('@backstage/catalog-client'); const MockedCatalogClient = CatalogClient as jest.Mock; const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; From 0e40a5915d828dd73294f80aa5ef3e3d5c51c361 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 19 Jan 2021 22:56:43 +0100 Subject: [PATCH 012/187] Fix client reference --- .../auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index ffd118884e..e1a45a4465 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -17,7 +17,7 @@ import { CatalogClient } from '@backstage/catalog-client'; import { CatalogIdentityClient } from './CatalogIdentityClient'; -jest.mock('./CatalogClient'); +jest.mock('@backstage/catalog-client'); const MockedCatalogClient = CatalogClient as jest.Mock; describe('CatalogIdentityClient', () => { From ca51082b3ff0e0744d3092e94bdeffa1c2177b3d Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 20 Jan 2021 01:11:12 +0100 Subject: [PATCH 013/187] Fix CatalogClient reference in create-app too --- .../default-app/packages/backend/src/plugins/scaffolder.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index c8bd3e5012..72f0f5099a 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -8,6 +8,7 @@ import { CatalogEntityClient, } from '@backstage/plugin-scaffolder-backend'; import { SingleHostDiscovery } from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; @@ -28,7 +29,8 @@ export default async function createPlugin({ const dockerClient = new Docker(); const discovery = SingleHostDiscovery.fromConfig(config); - const entityClient = new CatalogEntityClient({ discovery }); + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const entityClient = new CatalogEntityClient({ catalogClient }); return await createRouter({ preparers, From fdbea3ae20935a50c5075ef5222894cd01262ca7 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 20 Jan 2021 07:50:30 +0100 Subject: [PATCH 014/187] Move catalog-client dep to create-app --- packages/backend/package.json | 1 - .../templates/default-app/packages/backend/package.json.hbs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 8d215d5137..aa00407952 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -28,7 +28,6 @@ }, "dependencies": { "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-client": "^0.3.5", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/plugin-app-backend": "^0.3.5", diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 3fed72f07b..81f73768ca 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -19,6 +19,7 @@ "dependencies": { "app": "0.0.0", "@backstage/backend-common": "^{{version '@backstage/backend-common'}}", + "@backstage/catalog-client": "^{{version '@backstage/catalog-client'}}", "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", "@backstage/config": "^{{version '@backstage/config'}}", "@backstage/plugin-app-backend": "^{{version '@backstage/plugin-app-backend'}}", From 09b97cbe84931183423e3cd6abbb1e03ef17894f Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 20 Jan 2021 08:03:38 +0100 Subject: [PATCH 015/187] Add catalog-client dep again.. --- packages/backend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/package.json b/packages/backend/package.json index aa00407952..8d215d5137 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.5.1", + "@backstage/catalog-client": "^0.3.5", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/plugin-app-backend": "^0.3.5", From 05a6c4c39481949d36d0a7a1a22f4973a1208755 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Fri, 22 Jan 2021 22:16:54 +0100 Subject: [PATCH 016/187] Use RequestOptions --- packages/catalog-client/src/CatalogClient.ts | 50 ++++++++++++-------- packages/catalog-client/src/types.ts | 4 ++ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index c4f8c3d2b0..657841c000 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -26,6 +26,7 @@ import { AddLocationResponse, CatalogEntitiesRequest, CatalogListResponse, + CatalogRequestOptions, DiscoveryApi, } from './types'; @@ -37,15 +38,15 @@ export class CatalogClient { } async getLocationById( - token: string | undefined, id: String, + options?: CatalogRequestOptions, ): Promise { - return await this.getOptional(token, `/locations/${id}`); + return await this.getOptional(`/locations/${id}`, options); } async getEntities( - token: string | undefined, request?: CatalogEntitiesRequest, + options?: CatalogRequestOptions, ): Promise> { const { filter = {}, fields = [] } = request ?? {}; const params: string[] = []; @@ -66,36 +67,39 @@ export class CatalogClient { const query = params.length ? `?${params.join('&')}` : ''; const entities: Entity[] = await this.getRequired( - token, `/entities${query}`, + options, ); return { items: entities }; } async getEntityByName( - token: string | undefined, compoundName: EntityName, + options?: CatalogRequestOptions, ): Promise { const { kind, namespace = 'default', name } = compoundName; return this.getOptional( - token, `/entities/by-name/${kind}/${namespace}/${name}`, + options, ); } async addLocation( - token: string | undefined, { type = 'url', target, dryRun }: AddLocationRequest, + options?: CatalogRequestOptions, ): Promise { + const headers = { + 'Content-Type': 'application/json', + } as { [header: string]: string }; + if (options?.token) { + headers.authorization = `Bearer ${options.token}`; + } const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ dryRun ? '?dryRun=true' : '' }`, { - headers: { - authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + headers, method: 'POST', body: JSON.stringify({ type, target }), }, @@ -123,13 +127,13 @@ export class CatalogClient { } async getLocationByEntity( - token: string | undefined, entity: Entity, + options?: CatalogRequestOptions, ): Promise { const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION]; const all: { data: Location }[] = await this.getRequired( - token, '/locations', + options, ); return all .map(r => r.data) @@ -137,15 +141,15 @@ export class CatalogClient { } async removeEntityByUid( - token: string | undefined, uid: string, + options?: CatalogRequestOptions, ): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`, { - headers: { - authorization: `Bearer ${token}`, - }, + headers: options?.token + ? { authorization: `Bearer ${options.token}` } + : {}, method: 'DELETE', }, ); @@ -163,12 +167,14 @@ export class CatalogClient { // private async getRequired( - token: string | undefined, path: string, + options?: CatalogRequestOptions, ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; const response = await fetch(url, { - headers: token ? { authorization: `Bearer ${token}` } : {}, + headers: options?.token + ? { authorization: `Bearer ${options.token}` } + : {}, }); if (!response.ok) { @@ -181,12 +187,14 @@ export class CatalogClient { } private async getOptional( - token: string | undefined, path: string, + options?: CatalogRequestOptions, ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; const response = await fetch(url, { - headers: token ? { authorization: `Bearer ${token}` } : {}, + headers: options?.token + ? { authorization: `Bearer ${options.token}` } + : {}, }); if (!response.ok) { diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index e72317d444..66fbedd38e 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -25,6 +25,10 @@ export type CatalogListResponse = { items: T[]; }; +export type CatalogRequestOptions = { + token: string | undefined; +}; + export interface CatalogApi { getLocationById(id: String): Promise; getEntityByName(name: EntityName): Promise; From b3e9b08fd7c248ec2f2593e5a74033ed2fc052d0 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Fri, 22 Jan 2021 23:44:26 +0100 Subject: [PATCH 017/187] tsc --- .../catalog-client/src/CatalogClient.test.ts | 26 ++++++----- .../src/lib/catalog/CatalogIdentityClient.ts | 5 ++- .../src/providers/aws-alb/provider.test.ts | 24 +++++----- .../src/providers/aws-alb/provider.ts | 10 ++--- plugins/auth-backend/src/providers/types.ts | 2 +- .../src/api/CatalogImportClient.test.ts | 15 +++++++ .../components/ImportComponentForm.test.tsx | 16 +++++++ .../components/ImportComponentPage.test.tsx | 16 +++++++ plugins/catalog/src/CatalogClientWrapper.ts | 12 ++--- .../src/lib/catalog/CatalogEntityClient.ts | 13 +++--- yarn.lock | 45 +++++++++++++++++++ 11 files changed, 142 insertions(+), 42 deletions(-) diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 20cc35f423..d9bfb1eebd 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -72,7 +72,7 @@ describe('CatalogClient', () => { }); it('should entities from correct endpoint', async () => { - const response = await client.getEntities(token); + const response = await client.getEntities({}, { token }); expect(response).toEqual(defaultResponse); }); @@ -86,13 +86,16 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities(token, { - filter: { - a: '1', - b: ['2', '3'], - ö: '=', + const response = await client.getEntities( + { + filter: { + a: '1', + b: ['2', '3'], + ö: '=', + }, }, - }); + { token }, + ); expect(response.items).toEqual([]); }); @@ -107,9 +110,12 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities(token, { - fields: ['a.b', 'ö'], - }); + const response = await client.getEntities( + { + fields: ['a.b', 'ö'], + }, + { token }, + ); expect(response.items).toEqual([]); }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 0fad8122f4..9967a7be17 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -48,7 +48,10 @@ export class CatalogIdentityClient { filter[`metadata.annotations.${key}`] = value; } - const { items } = await this.catalogClient.getEntities(token, { filter }); + const { items } = await this.catalogClient.getEntities( + { filter }, + { token }, + ); if (items.length !== 1) { if (items.length > 1) { diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index d05b6bbfaf..536ba71f6b 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { getVoidLogger } from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; import express from 'express'; import { JWT } from 'jose'; @@ -43,6 +44,9 @@ jest.mock('cross-fetch', () => ({ }, })); +jest.mock('@backstage/catalog-client'); +const MockedCatalogClient = CatalogClient as jest.Mock; + const identityResolutionCallbackMock = async (): Promise> => { return { backstageIdentity: { @@ -67,15 +71,7 @@ beforeEach(() => { }); describe('AwsALBAuthProvider', () => { - const catalogApi = { - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - addLocation: jest.fn(), - getEntities: jest.fn(), - getLocationByEntity: jest.fn(), - getLocationById: jest.fn(), - removeEntityByUid: jest.fn(), - getEntityByName: jest.fn(), - }; + const catalogClient = new MockedCatalogClient(); const mockResponseSend = jest.fn(); const mockRequest = ({ @@ -95,7 +91,7 @@ describe('AwsALBAuthProvider', () => { describe('should transform to type OAuthResponse', () => { it('when JWT is valid and identity is resolved successfully', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackMock, issuer: 'foo', @@ -121,7 +117,7 @@ describe('AwsALBAuthProvider', () => { }); describe('should fail when', () => { it('JWT is missing', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackMock, issuer: 'foo', @@ -133,7 +129,7 @@ describe('AwsALBAuthProvider', () => { }); it('JWT is invalid', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackMock, issuer: 'foo', @@ -149,7 +145,7 @@ describe('AwsALBAuthProvider', () => { }); it('issuer is invalid', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackMock, issuer: 'foobar', @@ -163,7 +159,7 @@ describe('AwsALBAuthProvider', () => { }); it('identity resolution callback rejects', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackRejectedMock, issuer: 'foo', diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 61ea10947e..f8b61c15fd 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -25,7 +25,7 @@ import { KeyObject } from 'crypto'; import { Logger } from 'winston'; import NodeCache from 'node-cache'; import { JWT } from 'jose'; -import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogClient } from '@backstage/catalog-client'; const ALB_JWT_HEADER = 'x-amzn-oidc-data'; /** @@ -44,13 +44,13 @@ export const getJWTHeaders = (input: string) => { export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { private logger: Logger; - private readonly catalogClient: CatalogApi; + private readonly catalogClient: CatalogClient; private options: AwsAlbAuthProviderOptions; private readonly keyCache: NodeCache; constructor( logger: Logger, - catalogClient: CatalogApi, + catalogClient: CatalogClient, options: AwsAlbAuthProviderOptions, ) { this.logger = logger; @@ -108,14 +108,14 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { export const createAwsAlbProvider = ({ logger, - catalogApi, + catalogClient, config, identityResolver, }: AuthProviderFactoryOptions) => { const region = config.getString('region'); const issuer = config.getOptionalString('iss'); if (identityResolver !== undefined) { - return new AwsAlbAuthProvider(logger, catalogApi, { + return new AwsAlbAuthProvider(logger, catalogClient, { region, issuer, identityResolutionCallback: identityResolver, diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 1ab90ac522..b2f488546b 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -122,7 +122,7 @@ export type ExperimentalIdentityResolver = ( * An object containing information specific to the auth provider. */ payload: object, - catalogApi: CatalogApi, + catalogClient: CatalogClient, ) => Promise>; export type AuthProviderFactoryOptions = { diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 0e0cf4b323..321e4ba108 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -45,10 +45,25 @@ jest.mock('@octokit/rest', () => ({ })); describe('CatalogImportClient', () => { + const identityApi = { + getUserId: () => { + return 'user'; + }, + getProfile: () => { + return {}; + }, + getIdToken: () => { + return Promise.resolve('token'); + }, + signOut: () => { + return Promise.resolve(); + }, + }; describe('checkForExistingCatalogInfo', () => { const cic = new CatalogImportClient({ discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token') }, + identityApi, configApi: {} as any, }); it('should return the closest-to-root catalog-info from multiple responses', async () => { diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx index 38b1065325..9aef548856 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx @@ -35,6 +35,21 @@ describe('', () => { error$: jest.fn(), }; + const identityApi = { + getUserId: () => { + return 'user'; + }, + getProfile: () => { + return {}; + }, + getIdToken: () => { + return Promise.resolve('token'); + }, + signOut: () => { + return Promise.resolve(); + }, + }; + beforeEach(() => { apis = ApiRegistry.from([ [catalogApiRef, new CatalogClient({ discoveryApi: {} as DiscoveryApi })], @@ -45,6 +60,7 @@ describe('', () => { githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token'), }, + identityApi, configApi: {} as any, }), ], diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index f6ef07e1b2..90974e671d 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -70,6 +70,21 @@ describe('', () => { const server = setupServer(); msw.setupDefaultHandlers(server); + const identityApi = { + getUserId: () => { + return 'user'; + }, + getProfile: () => { + return {}; + }, + getIdToken: () => { + return Promise.resolve('token'); + }, + signOut: () => { + return Promise.resolve(); + }, + }; + beforeEach(() => { server.use( rest.post('https://backend.localhost/locations', (_, res, ctx) => { @@ -112,6 +127,7 @@ describe('', () => { githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token'), }, + identityApi, configApi: {} as any, }), ], diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts index fab551de48..1f06cd380b 100644 --- a/plugins/catalog/src/CatalogClientWrapper.ts +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -36,33 +36,33 @@ export class CatalogClientWrapper implements CatalogApi { async getLocationById(id: String): Promise { const token = await this.identityApi.getIdToken(); - return await this.client.getLocationById(token, id); + return await this.client.getLocationById(id, { token }); } async getEntities( request?: CatalogEntitiesRequest, ): Promise> { const token = await this.identityApi.getIdToken(); - return await this.client.getEntities(token, request); + return await this.client.getEntities(request, { token }); } async getEntityByName(compoundName: EntityName): Promise { const token = await this.identityApi.getIdToken(); - return await this.client.getEntityByName(token, compoundName); + return await this.client.getEntityByName(compoundName, { token }); } async addLocation(request: AddLocationRequest): Promise { const token = await this.identityApi.getIdToken(); - return await this.client.addLocation(token, request); + return await this.client.addLocation(request, { token }); } async getLocationByEntity(entity: Entity): Promise { const token = await this.identityApi.getIdToken(); - return await this.client.getLocationByEntity(token, entity); + return await this.client.getLocationByEntity(entity, { token }); } async removeEntityByUid(uid: string): Promise { const token = await this.identityApi.getIdToken(); - return await this.client.removeEntityByUid(token, uid); + return await this.client.removeEntityByUid(uid, { token }); } } diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 6c2909d360..0440a45f66 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -37,12 +37,15 @@ export class CatalogEntityClient { token: string | undefined, templateName: string, ): Promise { - const { items: templates } = (await this.catalogClient.getEntities(token, { - filter: { - kind: 'template', - 'metadata.name': templateName, + const { items: templates } = (await this.catalogClient.getEntities( + { + filter: { + kind: 'template', + 'metadata.name': templateName, + }, }, - })) as { items: TemplateEntityV1alpha1[] }; + { token }, + )) as { items: TemplateEntityV1alpha1[] }; if (templates.length !== 1) { if (templates.length > 1) { diff --git a/yarn.lock b/yarn.lock index aac0d3735b..1e17345f48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2645,6 +2645,46 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/core@^0.4.3": + version "0.4.4" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.4.4.tgz#83d5ce5c8dd8a9803badc1dde389b5bde963e959" + integrity sha512-UEZhPBC7DAZEOCRAYQ9XcXch5JDuPn+wYNp65Ik60SSz/RFjqhq8D3P7V074cMQqWLAddVDj5dUrNeN8N1gAUQ== + dependencies: + "@backstage/config" "^0.1.2" + "@backstage/core-api" "^0.2.8" + "@backstage/theme" "^0.2.2" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" + "@types/react" "^16.9" + "@types/react-sparklines" "^1.7.0" + classnames "^2.2.6" + clsx "^1.1.0" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-zoom "^2.0.0" + dagre "^0.8.5" + immer "^7.0.9" + lodash "^4.17.15" + material-table "^1.69.1" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "^3.0.0" + react "^16.12.0" + react-dom "^16.12.0" + react-helmet "6.1.0" + react-hook-form "^6.6.0" + react-markdown "^5.0.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^13.5.1" + react-use "^15.3.3" + remark-gfm "^1.0.0" + zen-observable "^0.8.15" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -15469,6 +15509,11 @@ immer@1.10.0: resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immer@^7.0.9: + version "7.0.15" + resolved "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz#dc3bc6db87401659d2e737c67a21b227c484a4ad" + integrity sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA== + immer@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" From 3a3cdfb1ca41be3410ccb5f5c5273a3d39e5e75c Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Sun, 24 Jan 2021 01:55:36 +0100 Subject: [PATCH 018/187] Backwards compatible scaffolder --- packages/backend/package.json | 1 - packages/backend/src/plugins/scaffolder.ts | 4 +--- .../default-app/packages/backend/package.json.hbs | 1 - .../default-app/packages/backend/src/plugins/scaffolder.ts | 4 +--- plugins/catalog/src/CatalogClientWrapper.test.ts | 4 +++- .../src/lib/catalog/CatalogEntityClient.ts | 7 +++++-- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 8d215d5137..aa00407952 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -28,7 +28,6 @@ }, "dependencies": { "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-client": "^0.3.5", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/plugin-app-backend": "^0.3.5", diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index bd3616116b..4e2257a46c 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -24,7 +24,6 @@ import { CatalogEntityClient, } from '@backstage/plugin-scaffolder-backend'; import { SingleHostDiscovery } from '@backstage/backend-common'; -import { CatalogClient } from '@backstage/catalog-client'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; @@ -45,8 +44,7 @@ export default async function createPlugin({ const dockerClient = new Docker(); const discovery = SingleHostDiscovery.fromConfig(config); - const catalogClient = new CatalogClient({ discoveryApi: discovery }); - const entityClient = new CatalogEntityClient({ catalogClient }); + const entityClient = new CatalogEntityClient({ discovery }); return await createRouter({ preparers, diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 81f73768ca..3fed72f07b 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -19,7 +19,6 @@ "dependencies": { "app": "0.0.0", "@backstage/backend-common": "^{{version '@backstage/backend-common'}}", - "@backstage/catalog-client": "^{{version '@backstage/catalog-client'}}", "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", "@backstage/config": "^{{version '@backstage/config'}}", "@backstage/plugin-app-backend": "^{{version '@backstage/plugin-app-backend'}}", diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index 72f0f5099a..c8bd3e5012 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -8,7 +8,6 @@ import { CatalogEntityClient, } from '@backstage/plugin-scaffolder-backend'; import { SingleHostDiscovery } from '@backstage/backend-common'; -import { CatalogClient } from '@backstage/catalog-client'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; @@ -29,8 +28,7 @@ export default async function createPlugin({ const dockerClient = new Docker(); const discovery = SingleHostDiscovery.fromConfig(config); - const catalogClient = new CatalogClient({ discoveryApi: discovery }); - const entityClient = new CatalogEntityClient({ catalogClient }); + const entityClient = new CatalogEntityClient({ discovery }); return await createRouter({ preparers, diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index e2aea16526..8ed53bddb0 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -58,7 +58,9 @@ describe('CatalogClientWrapper', () => { expect.assertions(2); await client.getEntities(); const getEntities = MockedCatalogClient.mock.instances[0].getEntities; - expect(getEntities).toHaveBeenCalledWith('fake-id-token', undefined); + expect(getEntities).toHaveBeenCalledWith(undefined, { + token: 'fake-id-token', + }); expect(getEntities).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 0440a45f66..7afbdd3fb9 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -16,6 +16,7 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { CatalogClient } from '@backstage/catalog-client'; +import { DiscoveryApi } from '@backstage/core'; import { ConflictError, NotFoundError } from '@backstage/backend-common'; /** @@ -24,8 +25,10 @@ import { ConflictError, NotFoundError } from '@backstage/backend-common'; export class CatalogEntityClient { private readonly catalogClient: CatalogClient; - constructor(options: { catalogClient: CatalogClient }) { - this.catalogClient = options.catalogClient; + constructor(options: { discovery: DiscoveryApi }) { + this.catalogClient = new CatalogClient({ + discoveryApi: options.discovery, + }); } /** From 3af90e2f467c232c8b04d6e50ac4e77128a6f5b9 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Sun, 24 Jan 2021 02:12:37 +0100 Subject: [PATCH 019/187] Fix test --- .../src/lib/catalog/CatalogIdentityClient.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index e1a45a4465..9137af5e83 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -33,12 +33,15 @@ describe('CatalogIdentityClient', () => { client.findUser(token, { annotations: { key: 'value' } }); const getEntities = MockedCatalogClient.mock.instances[0].getEntities; - expect(getEntities).toHaveBeenCalledWith(token, { - filter: { - kind: 'user', - 'metadata.annotations.key': 'value', + expect(getEntities).toHaveBeenCalledWith( + { + filter: { + kind: 'user', + 'metadata.annotations.key': 'value', + }, }, - }); + { token }, + ); expect(getEntities).toHaveBeenCalledTimes(1); }); }); From 7d7fe3d7881ebda31ccd354069afc362e9c39996 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 27 Jan 2021 00:10:19 +0100 Subject: [PATCH 020/187] Avoid importing core --- .../src/lib/catalog/CatalogEntityClient.ts | 9 ++++++--- plugins/scaffolder-backend/src/service/router.ts | 6 +++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 7afbdd3fb9..56b82760bd 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -16,8 +16,11 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { CatalogClient } from '@backstage/catalog-client'; -import { DiscoveryApi } from '@backstage/core'; -import { ConflictError, NotFoundError } from '@backstage/backend-common'; +import { + ConflictError, + NotFoundError, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; /** * A catalog client tailored for reading out entity data from the catalog. @@ -25,7 +28,7 @@ import { ConflictError, NotFoundError } from '@backstage/backend-common'; export class CatalogEntityClient { private readonly catalogClient: CatalogClient; - constructor(options: { discovery: DiscoveryApi }) { + constructor(options: { discovery: PluginEndpointDiscovery }) { this.catalogClient = new CatalogClient({ discoveryApi: options.discovery, }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 606aa05bc1..25b8c6e6f9 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -15,7 +15,6 @@ */ import { Config } from '@backstage/config'; -import { BackstageIdentity } from '@backstage/core'; import Docker from 'dockerode'; import express from 'express'; import { resolve as resolvePath } from 'path'; @@ -46,6 +45,11 @@ export interface RouterOptions { entityClient: CatalogEntityClient; } +// Avoid import of @backstage/core +type BackstageIdentity = { + idToken: string; +}; + export async function createRouter( options: RouterOptions, ): Promise { From 97d13a88896e63e831303a954f688fe512685ed0 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 27 Jan 2021 02:30:33 +0100 Subject: [PATCH 021/187] Use request options --- .../src/lib/catalog/CatalogIdentityClient.test.ts | 2 +- .../src/lib/catalog/CatalogIdentityClient.ts | 4 +++- plugins/auth-backend/src/providers/google/provider.ts | 11 +++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 9137af5e83..7eb50c92bf 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -30,7 +30,7 @@ describe('CatalogIdentityClient', () => { catalogClient: new MockedCatalogClient(), }); - client.findUser(token, { annotations: { key: 'value' } }); + client.findUser({ annotations: { key: 'value' } }, { token }); const getEntities = MockedCatalogClient.mock.instances[0].getEntities; expect(getEntities).toHaveBeenCalledWith( diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 9967a7be17..6c052321a1 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -17,6 +17,7 @@ import { ConflictError, NotFoundError } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; import { UserEntity } from '@backstage/catalog-model'; +import { CatalogIdentityRequestOptions } from './types'; type UserQuery = { annotations: Record; @@ -38,9 +39,10 @@ export class CatalogIdentityClient { * Throws a NotFoundError or ConflictError if 0 or multiple users are found. */ async findUser( - token: string | undefined, query: UserQuery, + options?: CatalogIdentityRequestOptions, ): Promise { + const token = options?.token; const filter: Record = { kind: 'user', }; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 340ec46529..15c2172e6e 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -157,11 +157,14 @@ export class GoogleAuthProvider implements OAuthHandlers { const token = await this.tokenIssuer.issueToken({ claims: { sub: 'backstage.io/auth-backend' }, }); - const user = await this.identityClient.findUser(token, { - annotations: { - 'google.com/email': profile.email, + const user = await this.identityClient.findUser( + { + annotations: { + 'google.com/email': profile.email, + }, }, - }); + { token }, + ); return { ...response, From 1476950a127ce7bdde25e4baa560f3287c02c5f6 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 27 Jan 2021 02:30:59 +0100 Subject: [PATCH 022/187] Increase code coverage --- .../catalog-client/src/CatalogClient.test.ts | 47 +++++++++ plugins/auth-backend/src/lib/catalog/types.ts | 19 ++++ .../catalog/src/CatalogClientWrapper.test.ts | 97 ++++++++++++++++++- 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 plugins/auth-backend/src/lib/catalog/types.ts diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index d9bfb1eebd..19da45e4ec 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -120,4 +120,51 @@ describe('CatalogClient', () => { expect(response.items).toEqual([]); }); }); + + describe('getLocationById', () => { + const defaultResponse = { + data: { + id: '42', + }, + }; + + beforeEach(() => { + server.use( + rest.get(`${mockBaseUrl}/locations/42`, (_, res, ctx) => { + return res(ctx.json(defaultResponse)); + }), + ); + }); + + it('should locations from correct endpoint', async () => { + const response = await client.getLocationById('42', { token }); + expect(response).toEqual(defaultResponse); + }); + + it('forwards authorization token', async () => { + expect.assertions(1); + + server.use( + rest.get(`${mockBaseUrl}/locations/42`, (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe(`Bearer ${token}`); + return res(ctx.json(defaultResponse)); + }), + ); + + await client.getLocationById('42', { token }); + }); + + it('skips authorization header if token is omitted', async () => { + expect.assertions(1); + + server.use( + rest.get(`${mockBaseUrl}/locations/42`, (req, res, ctx) => { + expect(req.headers.get('authorization')).toBeNull(); + return res(ctx.json(defaultResponse)); + }), + ); + + await client.getLocationById('42'); + }); + }); }); diff --git a/plugins/auth-backend/src/lib/catalog/types.ts b/plugins/auth-backend/src/lib/catalog/types.ts new file mode 100644 index 0000000000..389cc14bc5 --- /dev/null +++ b/plugins/auth-backend/src/lib/catalog/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type CatalogIdentityRequestOptions = { + token: string | undefined; +}; diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index 8ed53bddb0..9d6f8b5c57 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -41,9 +41,24 @@ const identityApi: IdentityApi = { return Promise.resolve(); }, }; +const guestIdentityApi: IdentityApi = { + getUserId() { + return 'guest'; + }, + getProfile() { + return {}; + }, + async getIdToken() { + return Promise.resolve(undefined); + }, + async signOut() { + return Promise.resolve(); + }, +}; describe('CatalogClientWrapper', () => { - let client: CatalogClientWrapper; + let client; + let guestClient; beforeEach(() => { MockedCatalogClient.mockClear(); @@ -51,6 +66,10 @@ describe('CatalogClientWrapper', () => { client: new MockedCatalogClient({ discoveryApi }), identityApi, }); + guestClient = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi: guestIdentityApi, + }); }); describe('getEntities', () => { @@ -64,4 +83,80 @@ describe('CatalogClientWrapper', () => { expect(getEntities).toHaveBeenCalledTimes(1); }); }); + + describe('getLocationById', () => { + it('omits authorization token when guest', async () => { + expect.assertions(2); + await guestClient.getLocationById('42'); + const getLocationById = + MockedCatalogClient.mock.instances[0].getLocationById; + expect(getLocationById).toHaveBeenCalledWith('42', {}); + expect(getLocationById).toHaveBeenCalledTimes(1); + }); + }); + + describe('getEntityByName', () => { + const name = { + kind: 'kind', + namespace: 'namespace', + name: 'name', + }; + it('injects authorization token', async () => { + expect.assertions(2); + await client.getEntityByName(name); + const getEntityByName = + MockedCatalogClient.mock.instances[0].getEntityByName; + expect(getEntityByName).toHaveBeenCalledWith(name, { + token: 'fake-id-token', + }); + expect(getEntityByName).toHaveBeenCalledTimes(1); + }); + }); + + describe('addLocation', () => { + const location = { target: 'target' }; + it('injects authorization token', async () => { + expect.assertions(2); + await client.addLocation(location); + const addLocation = MockedCatalogClient.mock.instances[0].addLocation; + expect(addLocation).toHaveBeenCalledWith(location, { + token: 'fake-id-token', + }); + expect(addLocation).toHaveBeenCalledTimes(1); + }); + }); + + describe('getLocationByEntity', () => { + const entity = { + apiVersion: 'apiVersion', + kind: 'kind', + metadata: { + name: 'name', + }, + }; + it('injects authorization token', async () => { + expect.assertions(2); + await client.getLocationByEntity(entity); + const getLocationByEntity = + MockedCatalogClient.mock.instances[0].getLocationByEntity; + expect(getLocationByEntity).toHaveBeenCalledWith(entity, { + token: 'fake-id-token', + }); + expect(getLocationByEntity).toHaveBeenCalledTimes(1); + }); + }); + + describe('removeEntityByUid', () => { + it('injects authorization token', async () => { + const uid = 'uid'; + expect.assertions(2); + await client.removeEntityByUid(uid); + const removeEntityByUid = + MockedCatalogClient.mock.instances[0].removeEntityByUid; + expect(removeEntityByUid).toHaveBeenCalledWith(uid, { + token: 'fake-id-token', + }); + expect(removeEntityByUid).toHaveBeenCalledTimes(1); + }); + }); }); From cf074558429f7b36f4df319f0af47401e92850ec Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 27 Jan 2021 02:48:04 +0100 Subject: [PATCH 023/187] Type those variables --- plugins/catalog/src/CatalogClientWrapper.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index 9d6f8b5c57..de60ca8dd2 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -57,8 +57,8 @@ const guestIdentityApi: IdentityApi = { }; describe('CatalogClientWrapper', () => { - let client; - let guestClient; + let client: CatalogClientWrapper; + let guestClient: CatalogClientWrapper; beforeEach(() => { MockedCatalogClient.mockClear(); From 2688210621507d50dfb1f4ed33251dc112a50e3c Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 27 Jan 2021 09:52:26 +0100 Subject: [PATCH 024/187] use the second mock --- plugins/catalog/src/CatalogClientWrapper.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index de60ca8dd2..0306d10b5d 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -89,7 +89,7 @@ describe('CatalogClientWrapper', () => { expect.assertions(2); await guestClient.getLocationById('42'); const getLocationById = - MockedCatalogClient.mock.instances[0].getLocationById; + MockedCatalogClient.mock.instances[1].getLocationById; expect(getLocationById).toHaveBeenCalledWith('42', {}); expect(getLocationById).toHaveBeenCalledTimes(1); }); From cee1933d77eb28ad87b2a3417ffee24d60eb103a Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 27 Jan 2021 13:21:19 +0100 Subject: [PATCH 025/187] Trying to make tests work on Github too --- .../catalog/src/CatalogClientWrapper.test.ts | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index 0306d10b5d..d95d274ee9 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -57,24 +57,17 @@ const guestIdentityApi: IdentityApi = { }; describe('CatalogClientWrapper', () => { - let client: CatalogClientWrapper; - let guestClient: CatalogClientWrapper; - beforeEach(() => { MockedCatalogClient.mockClear(); - client = new CatalogClientWrapper({ - client: new MockedCatalogClient({ discoveryApi }), - identityApi, - }); - guestClient = new CatalogClientWrapper({ - client: new MockedCatalogClient({ discoveryApi }), - identityApi: guestIdentityApi, - }); }); describe('getEntities', () => { it('injects authorization token', async () => { expect.assertions(2); + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); await client.getEntities(); const getEntities = MockedCatalogClient.mock.instances[0].getEntities; expect(getEntities).toHaveBeenCalledWith(undefined, { @@ -87,9 +80,13 @@ describe('CatalogClientWrapper', () => { describe('getLocationById', () => { it('omits authorization token when guest', async () => { expect.assertions(2); - await guestClient.getLocationById('42'); + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi: guestIdentityApi, + }); + await client.getLocationById('42'); const getLocationById = - MockedCatalogClient.mock.instances[1].getLocationById; + MockedCatalogClient.mock.instances[0].getLocationById; expect(getLocationById).toHaveBeenCalledWith('42', {}); expect(getLocationById).toHaveBeenCalledTimes(1); }); @@ -103,6 +100,10 @@ describe('CatalogClientWrapper', () => { }; it('injects authorization token', async () => { expect.assertions(2); + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); await client.getEntityByName(name); const getEntityByName = MockedCatalogClient.mock.instances[0].getEntityByName; @@ -117,6 +118,10 @@ describe('CatalogClientWrapper', () => { const location = { target: 'target' }; it('injects authorization token', async () => { expect.assertions(2); + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); await client.addLocation(location); const addLocation = MockedCatalogClient.mock.instances[0].addLocation; expect(addLocation).toHaveBeenCalledWith(location, { @@ -136,6 +141,10 @@ describe('CatalogClientWrapper', () => { }; it('injects authorization token', async () => { expect.assertions(2); + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); await client.getLocationByEntity(entity); const getLocationByEntity = MockedCatalogClient.mock.instances[0].getLocationByEntity; @@ -150,6 +159,10 @@ describe('CatalogClientWrapper', () => { it('injects authorization token', async () => { const uid = 'uid'; expect.assertions(2); + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); await client.removeEntityByUid(uid); const removeEntityByUid = MockedCatalogClient.mock.instances[0].removeEntityByUid; From 5ba0804189667cf4804446d7a850b0ca803ec1d5 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Fri, 29 Jan 2021 16:38:53 +0100 Subject: [PATCH 026/187] Get clean yarn from master --- yarn.lock | 45 --------------------------------------------- 1 file changed, 45 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1e17345f48..aac0d3735b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2645,46 +2645,6 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/core@^0.4.3": - version "0.4.4" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.4.4.tgz#83d5ce5c8dd8a9803badc1dde389b5bde963e959" - integrity sha512-UEZhPBC7DAZEOCRAYQ9XcXch5JDuPn+wYNp65Ik60SSz/RFjqhq8D3P7V074cMQqWLAddVDj5dUrNeN8N1gAUQ== - dependencies: - "@backstage/config" "^0.1.2" - "@backstage/core-api" "^0.2.8" - "@backstage/theme" "^0.2.2" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9" - "@types/react-sparklines" "^1.7.0" - classnames "^2.2.6" - clsx "^1.1.0" - d3-selection "^2.0.0" - d3-shape "^2.0.0" - d3-zoom "^2.0.0" - dagre "^0.8.5" - immer "^7.0.9" - lodash "^4.17.15" - material-table "^1.69.1" - prop-types "^15.7.2" - qs "^6.9.4" - rc-progress "^3.0.0" - react "^16.12.0" - react-dom "^16.12.0" - react-helmet "6.1.0" - react-hook-form "^6.6.0" - react-markdown "^5.0.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-sparklines "^1.7.0" - react-syntax-highlighter "^13.5.1" - react-use "^15.3.3" - remark-gfm "^1.0.0" - zen-observable "^0.8.15" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -15509,11 +15469,6 @@ immer@1.10.0: resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== -immer@^7.0.9: - version "7.0.15" - resolved "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz#dc3bc6db87401659d2e737c67a21b227c484a4ad" - integrity sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA== - immer@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" From 320708e17175ee2e3dd8eec2c4b37f83d928c24d Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Sat, 30 Jan 2021 04:03:43 +0100 Subject: [PATCH 027/187] Only add authorization header if token exists --- .../src/api/CatalogImportClient.ts | 22 ++++++++++++------- plugins/fossa/src/api/FossaClient.ts | 6 ++--- .../src/api/KubernetesBackendClient.ts | 11 ++++++---- plugins/rollbar/src/api/RollbarClient.ts | 4 +--- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 9b52de0ba4..b812e0dda9 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -49,13 +49,16 @@ export class CatalogImportClient implements CatalogImportApi { repo: string; }): Promise { const idToken = await this.identityApi.getIdToken(); + const headers = { + 'Content-Type': 'application/json', + } as { [header: string]: string }; + if (idToken) { + headers.authorization = `Bearer ${idToken}`; + } const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`, { - headers: { - authorization: `Bearer ${idToken}`, - 'Content-Type': 'application/json', - }, + headers, method: 'POST', body: JSON.stringify({ location: { type: 'url', target: repo }, @@ -80,13 +83,16 @@ export class CatalogImportClient implements CatalogImportApi { location: string; }): Promise { const idToken = await this.identityApi.getIdToken(); + const headers = { + 'Content-Type': 'application/json', + } as { [header: string]: string }; + if (idToken) { + headers.authorization = `Bearer ${idToken}`; + } const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, { - headers: { - authorization: `Bearer ${idToken}`, - 'Content-Type': 'application/json', - }, + headers, method: 'POST', body: JSON.stringify({ type: 'url', diff --git a/plugins/fossa/src/api/FossaClient.ts b/plugins/fossa/src/api/FossaClient.ts index 33c8c65c5f..4428ec7dbb 100644 --- a/plugins/fossa/src/api/FossaClient.ts +++ b/plugins/fossa/src/api/FossaClient.ts @@ -39,11 +39,9 @@ export class FossaClient implements FossaApi { private async callApi(path: string): Promise { const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`; - const headers: Record = { - authorization: await `Bearer ${this.identityApi.getIdToken()}`, - }; + const idToken = await this.identityApi.getIdToken(); const response = await fetch(`${apiUrl}/${path}`, { - headers, + headers: idToken ? { authorization: `Bearer ${idToken}` } : {}, }); if (response.status === 200) { return await response.json(); diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index e005b29517..96bdb9420f 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -39,12 +39,15 @@ export class KubernetesBackendClient implements KubernetesApi { ): Promise { const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`; const idToken = await this.identityApi.getIdToken(); + const headers = { + 'Content-Type': 'application/json', + } as { [header: string]: string }; + if (idToken) { + headers.authorization = `Bearer ${idToken}`; + } const response = await fetch(url, { method: 'POST', - headers: { - authorization: `Bearer ${idToken}`, - 'Content-Type': 'application/json', - }, + headers, body: JSON.stringify(requestBody), }); diff --git a/plugins/rollbar/src/api/RollbarClient.ts b/plugins/rollbar/src/api/RollbarClient.ts index 1041accf19..dfc82b09e5 100644 --- a/plugins/rollbar/src/api/RollbarClient.ts +++ b/plugins/rollbar/src/api/RollbarClient.ts @@ -60,9 +60,7 @@ export class RollbarClient implements RollbarApi { const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`; const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { - headers: { - authorization: `Bearer ${idToken}`, - }, + headers: idToken ? { authorization: `Bearer ${idToken}` } : {}, }); if (!response.ok) { From 9729d9e572a589c0b2da33a179b8c0547a5faadb Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Sat, 30 Jan 2021 04:14:17 +0100 Subject: [PATCH 028/187] Use request options --- .../src/lib/catalog/CatalogEntityClient.ts | 4 ++- .../src/lib/catalog/types.ts | 32 +++++++++++++++++++ .../scaffolder-backend/src/service/router.ts | 7 ++-- 3 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 plugins/scaffolder-backend/src/lib/catalog/types.ts diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 56b82760bd..ed8b28552d 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -21,6 +21,7 @@ import { NotFoundError, PluginEndpointDiscovery, } from '@backstage/backend-common'; +import { CatalogEntityRequestOptions } from './types'; /** * A catalog client tailored for reading out entity data from the catalog. @@ -40,9 +41,10 @@ export class CatalogEntityClient { * Throws a NotFoundError or ConflictError if 0 or multiple templates are found. */ async findTemplate( - token: string | undefined, templateName: string, + options?: CatalogEntityRequestOptions, ): Promise { + const token = options?.token; const { items: templates } = (await this.catalogClient.getEntities( { filter: { diff --git a/plugins/scaffolder-backend/src/lib/catalog/types.ts b/plugins/scaffolder-backend/src/lib/catalog/types.ts new file mode 100644 index 0000000000..188efbd9d0 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/catalog/types.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type CatalogEntityRequestOptions = { + token: string | undefined; +}; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 25b8c6e6f9..cf10643de3 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -104,10 +104,9 @@ export async function createRouter( // Forward authorization from client const user = req.user as BackstageIdentity; - const template = await entityClient.findTemplate( - user?.idToken, - templateName, - ); + const template = await entityClient.findTemplate(templateName, { + token: user?.idToken, + }); const validationResult: ValidatorResult = validate( values, From 987fa8b24fce3d32820be7d0dcf16988775a0afa Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Sun, 31 Jan 2021 01:17:11 +0100 Subject: [PATCH 029/187] Forward authorization header token --- plugins/scaffolder-backend/package.json | 1 + plugins/scaffolder-backend/src/service/router.ts | 9 ++------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index c9a439bea3..ecf4978ff2 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -34,6 +34,7 @@ "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/integration": "^0.3.1", + "@backstage/plugin-auth-backend": "^0.2.12", "@gitbeaker/core": "^28.0.2", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index cf10643de3..e522417945 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -15,6 +15,7 @@ */ import { Config } from '@backstage/config'; +import { IdentityClient } from '@backstage/plugin-auth-backend'; import Docker from 'dockerode'; import express from 'express'; import { resolve as resolvePath } from 'path'; @@ -45,11 +46,6 @@ export interface RouterOptions { entityClient: CatalogEntityClient; } -// Avoid import of @backstage/core -type BackstageIdentity = { - idToken: string; -}; - export async function createRouter( options: RouterOptions, ): Promise { @@ -103,9 +99,8 @@ export async function createRouter( }; // Forward authorization from client - const user = req.user as BackstageIdentity; const template = await entityClient.findTemplate(templateName, { - token: user?.idToken, + token: IdentityClient.getBearerToken(req.headers.authorization), }); const validationResult: ValidatorResult = validate( From 124d21c20afc20c89495ca5076f60d597189ba9d Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Wed, 3 Feb 2021 02:00:32 +0100 Subject: [PATCH 030/187] Refactor to use context argument --- packages/catalog-client/src/CatalogClient.ts | 51 ++++++------- packages/catalog-client/src/index.ts | 1 + packages/catalog-client/src/types.ts | 27 +++++-- .../lib/catalog/CatalogIdentityClient.test.ts | 26 ++++--- .../src/lib/catalog/CatalogIdentityClient.ts | 20 ++---- plugins/auth-backend/src/lib/catalog/types.ts | 19 ----- .../src/providers/aws-alb/provider.test.ts | 24 ++++--- .../src/providers/aws-alb/provider.ts | 10 +-- .../src/providers/google/provider.ts | 4 +- plugins/auth-backend/src/providers/types.ts | 6 +- plugins/auth-backend/src/service/router.ts | 4 +- plugins/catalog/src/CatalogClientWrapper.ts | 72 ++++++++++++++----- plugins/catalog/src/plugin.ts | 1 - .../src/lib/catalog/CatalogEntityClient.ts | 8 +-- .../src/lib/catalog/types.ts | 32 --------- 15 files changed, 149 insertions(+), 156 deletions(-) delete mode 100644 plugins/auth-backend/src/lib/catalog/types.ts delete mode 100644 plugins/scaffolder-backend/src/lib/catalog/types.ts diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 657841c000..b0c28bc284 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -24,13 +24,14 @@ import fetch from 'cross-fetch'; import { AddLocationRequest, AddLocationResponse, + ApiContext, + CatalogApi, CatalogEntitiesRequest, CatalogListResponse, - CatalogRequestOptions, DiscoveryApi, } from './types'; -export class CatalogClient { +export class CatalogClient implements CatalogApi { private readonly discoveryApi: DiscoveryApi; constructor(options: { discoveryApi: DiscoveryApi }) { @@ -39,14 +40,14 @@ export class CatalogClient { async getLocationById( id: String, - options?: CatalogRequestOptions, + context?: ApiContext, ): Promise { - return await this.getOptional(`/locations/${id}`, options); + return await this.getOptional(`/locations/${id}`, context); } async getEntities( request?: CatalogEntitiesRequest, - options?: CatalogRequestOptions, + context?: ApiContext, ): Promise> { const { filter = {}, fields = [] } = request ?? {}; const params: string[] = []; @@ -68,31 +69,31 @@ export class CatalogClient { const query = params.length ? `?${params.join('&')}` : ''; const entities: Entity[] = await this.getRequired( `/entities${query}`, - options, + context, ); return { items: entities }; } async getEntityByName( compoundName: EntityName, - options?: CatalogRequestOptions, + context?: ApiContext, ): Promise { const { kind, namespace = 'default', name } = compoundName; return this.getOptional( `/entities/by-name/${kind}/${namespace}/${name}`, - options, + context, ); } async addLocation( { type = 'url', target, dryRun }: AddLocationRequest, - options?: CatalogRequestOptions, + context?: ApiContext, ): Promise { const headers = { 'Content-Type': 'application/json', } as { [header: string]: string }; - if (options?.token) { - headers.authorization = `Bearer ${options.token}`; + if (context?.token) { + headers.authorization = `Bearer ${context.token}`; } const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ @@ -128,27 +129,24 @@ export class CatalogClient { async getLocationByEntity( entity: Entity, - options?: CatalogRequestOptions, + context?: ApiContext, ): Promise { const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION]; const all: { data: Location }[] = await this.getRequired( '/locations', - options, + context, ); return all .map(r => r.data) .find(l => locationCompound === `${l.type}:${l.target}`); } - async removeEntityByUid( - uid: string, - options?: CatalogRequestOptions, - ): Promise { + async removeEntityByUid(uid: string, context?: ApiContext): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`, { - headers: options?.token - ? { authorization: `Bearer ${options.token}` } + headers: context?.token + ? { authorization: `Bearer ${context.token}` } : {}, method: 'DELETE', }, @@ -166,14 +164,11 @@ export class CatalogClient { // Private methods // - private async getRequired( - path: string, - options?: CatalogRequestOptions, - ): Promise { + private async getRequired(path: string, context?: ApiContext): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; const response = await fetch(url, { - headers: options?.token - ? { authorization: `Bearer ${options.token}` } + headers: context?.token + ? { authorization: `Bearer ${context.token}` } : {}, }); @@ -188,12 +183,12 @@ export class CatalogClient { private async getOptional( path: string, - options?: CatalogRequestOptions, + context?: ApiContext, ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; const response = await fetch(url, { - headers: options?.token - ? { authorization: `Bearer ${options.token}` } + headers: context?.token + ? { authorization: `Bearer ${context.token}` } : {}, }); diff --git a/packages/catalog-client/src/index.ts b/packages/catalog-client/src/index.ts index c5a626e25b..11003580a0 100644 --- a/packages/catalog-client/src/index.ts +++ b/packages/catalog-client/src/index.ts @@ -18,6 +18,7 @@ export { CatalogClient } from './CatalogClient'; export type { AddLocationRequest, AddLocationResponse, + ApiContext, CatalogApi, CatalogEntitiesRequest, CatalogListResponse, diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index 66fbedd38e..9a0ec90c16 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -25,19 +25,32 @@ export type CatalogListResponse = { items: T[]; }; -export type CatalogRequestOptions = { - token: string | undefined; +export type ApiContext = { + token?: string; }; export interface CatalogApi { - getLocationById(id: String): Promise; - getEntityByName(name: EntityName): Promise; + getLocationById( + id: String, + context?: ApiContext, + ): Promise; + getEntityByName( + name: EntityName, + context?: ApiContext, + ): Promise; getEntities( request?: CatalogEntitiesRequest, + context?: ApiContext, ): Promise>; - addLocation(location: AddLocationRequest): Promise; - getLocationByEntity(entity: Entity): Promise; - removeEntityByUid(uid: string): Promise; + addLocation( + location: AddLocationRequest, + context?: ApiContext, + ): Promise; + getLocationByEntity( + entity: Entity, + context?: ApiContext, + ): Promise; + removeEntityByUid(uid: string, context?: ApiContext): Promise; } export type AddLocationRequest = { diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 7eb50c92bf..4d4bcaf2bf 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -14,34 +14,38 @@ * limitations under the License. */ -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi } from '@backstage/catalog-client'; +import { UserEntity } from '@backstage/catalog-model'; import { CatalogIdentityClient } from './CatalogIdentityClient'; -jest.mock('@backstage/catalog-client'); -const MockedCatalogClient = CatalogClient as jest.Mock; - describe('CatalogIdentityClient', () => { - const token = 'fake-id-token'; + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + }; afterEach(() => jest.resetAllMocks()); it('passes through the correct search params', async () => { + catalogApi.getEntities.mockResolvedValueOnce({ items: [{} as UserEntity] }); const client = new CatalogIdentityClient({ - catalogClient: new MockedCatalogClient(), + catalogApi: catalogApi as CatalogApi, }); - client.findUser({ annotations: { key: 'value' } }, { token }); + client.findUser({ annotations: { key: 'value' } }); - const getEntities = MockedCatalogClient.mock.instances[0].getEntities; - expect(getEntities).toHaveBeenCalledWith( + expect(catalogApi.getEntities).toBeCalledWith( { filter: { kind: 'user', 'metadata.annotations.key': 'value', }, }, - { token }, + undefined, ); - expect(getEntities).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 6c052321a1..3892b83da0 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -15,9 +15,8 @@ */ import { ConflictError, NotFoundError } from '@backstage/backend-common'; -import { CatalogClient } from '@backstage/catalog-client'; +import { ApiContext, CatalogApi } from '@backstage/catalog-client'; import { UserEntity } from '@backstage/catalog-model'; -import { CatalogIdentityRequestOptions } from './types'; type UserQuery = { annotations: Record; @@ -27,10 +26,10 @@ type UserQuery = { * A catalog client tailored for reading out identity data from the catalog. */ export class CatalogIdentityClient { - private readonly catalogClient: CatalogClient; + private readonly catalogApi: CatalogApi; - constructor(options: { catalogClient: CatalogClient }) { - this.catalogClient = options.catalogClient; + constructor(options: { catalogApi: CatalogApi }) { + this.catalogApi = options.catalogApi; } /** @@ -38,11 +37,7 @@ export class CatalogIdentityClient { * * Throws a NotFoundError or ConflictError if 0 or multiple users are found. */ - async findUser( - query: UserQuery, - options?: CatalogIdentityRequestOptions, - ): Promise { - const token = options?.token; + async findUser(query: UserQuery, context?: ApiContext): Promise { const filter: Record = { kind: 'user', }; @@ -50,10 +45,7 @@ export class CatalogIdentityClient { filter[`metadata.annotations.${key}`] = value; } - const { items } = await this.catalogClient.getEntities( - { filter }, - { token }, - ); + const { items } = await this.catalogApi.getEntities({ filter }, context); if (items.length !== 1) { if (items.length > 1) { diff --git a/plugins/auth-backend/src/lib/catalog/types.ts b/plugins/auth-backend/src/lib/catalog/types.ts deleted file mode 100644 index 389cc14bc5..0000000000 --- a/plugins/auth-backend/src/lib/catalog/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export type CatalogIdentityRequestOptions = { - token: string | undefined; -}; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index 536ba71f6b..d05b6bbfaf 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { getVoidLogger } from '@backstage/backend-common'; -import { CatalogClient } from '@backstage/catalog-client'; import express from 'express'; import { JWT } from 'jose'; @@ -44,9 +43,6 @@ jest.mock('cross-fetch', () => ({ }, })); -jest.mock('@backstage/catalog-client'); -const MockedCatalogClient = CatalogClient as jest.Mock; - const identityResolutionCallbackMock = async (): Promise> => { return { backstageIdentity: { @@ -71,7 +67,15 @@ beforeEach(() => { }); describe('AwsALBAuthProvider', () => { - const catalogClient = new MockedCatalogClient(); + const catalogApi = { + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + addLocation: jest.fn(), + getEntities: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + getEntityByName: jest.fn(), + }; const mockResponseSend = jest.fn(); const mockRequest = ({ @@ -91,7 +95,7 @@ describe('AwsALBAuthProvider', () => { describe('should transform to type OAuthResponse', () => { it('when JWT is valid and identity is resolved successfully', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackMock, issuer: 'foo', @@ -117,7 +121,7 @@ describe('AwsALBAuthProvider', () => { }); describe('should fail when', () => { it('JWT is missing', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackMock, issuer: 'foo', @@ -129,7 +133,7 @@ describe('AwsALBAuthProvider', () => { }); it('JWT is invalid', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackMock, issuer: 'foo', @@ -145,7 +149,7 @@ describe('AwsALBAuthProvider', () => { }); it('issuer is invalid', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackMock, issuer: 'foobar', @@ -159,7 +163,7 @@ describe('AwsALBAuthProvider', () => { }); it('identity resolution callback rejects', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogClient, { + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { region: 'us-west-2', identityResolutionCallback: identityResolutionCallbackRejectedMock, issuer: 'foo', diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index f8b61c15fd..61ea10947e 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -25,7 +25,7 @@ import { KeyObject } from 'crypto'; import { Logger } from 'winston'; import NodeCache from 'node-cache'; import { JWT } from 'jose'; -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi } from '@backstage/catalog-client'; const ALB_JWT_HEADER = 'x-amzn-oidc-data'; /** @@ -44,13 +44,13 @@ export const getJWTHeaders = (input: string) => { export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { private logger: Logger; - private readonly catalogClient: CatalogClient; + private readonly catalogClient: CatalogApi; private options: AwsAlbAuthProviderOptions; private readonly keyCache: NodeCache; constructor( logger: Logger, - catalogClient: CatalogClient, + catalogClient: CatalogApi, options: AwsAlbAuthProviderOptions, ) { this.logger = logger; @@ -108,14 +108,14 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { export const createAwsAlbProvider = ({ logger, - catalogClient, + catalogApi, config, identityResolver, }: AuthProviderFactoryOptions) => { const region = config.getString('region'); const issuer = config.getOptionalString('iss'); if (identityResolver !== undefined) { - return new AwsAlbAuthProvider(logger, catalogClient, { + return new AwsAlbAuthProvider(logger, catalogApi, { region, issuer, identityResolutionCallback: identityResolver, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 15c2172e6e..848a02bad6 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -190,7 +190,7 @@ export const createGoogleProvider: AuthProviderFactory = ({ config, logger, tokenIssuer, - catalogClient, + catalogApi, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); @@ -203,7 +203,7 @@ export const createGoogleProvider: AuthProviderFactory = ({ callbackUrl, logger, tokenIssuer, - identityClient: new CatalogIdentityClient({ catalogClient }), + identityClient: new CatalogIdentityClient({ catalogApi }), }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index b2f488546b..1a4e7b118a 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -15,7 +15,7 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -122,7 +122,7 @@ export type ExperimentalIdentityResolver = ( * An object containing information specific to the auth provider. */ payload: object, - catalogClient: CatalogClient, + catalogApi: CatalogApi, ) => Promise>; export type AuthProviderFactoryOptions = { @@ -132,8 +132,8 @@ export type AuthProviderFactoryOptions = { logger: Logger; tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; + catalogApi: CatalogApi; identityResolver?: ExperimentalIdentityResolver; - catalogClient: CatalogClient; }; export type AuthProviderFactory = ( diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 859c2a4846..85d60661e9 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -66,7 +66,7 @@ export async function createRouter({ keyDurationSeconds, logger: logger.child({ component: 'token-factory' }), }); - const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const catalogApi = new CatalogClient({ discoveryApi: discovery }); const secret = config.getOptionalString('auth.session.secret'); if (secret) { @@ -103,7 +103,7 @@ export async function createRouter({ logger, tokenIssuer, discovery, - catalogClient, + catalogApi, }); const r = Router(); diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts index 1f06cd380b..19bf77c91d 100644 --- a/plugins/catalog/src/CatalogClientWrapper.ts +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -18,6 +18,7 @@ import { Entity, EntityName, Location } from '@backstage/catalog-model'; import { AddLocationRequest, AddLocationResponse, + ApiContext, CatalogApi, CatalogEntitiesRequest, CatalogListResponse, @@ -25,6 +26,9 @@ import { } from '@backstage/catalog-client'; import { IdentityApi } from '@backstage/core'; +/** + * CatalogClient wrapper that injects identity token for all requests + */ export class CatalogClientWrapper implements CatalogApi { private readonly identityApi: IdentityApi; private readonly client: CatalogClient; @@ -34,35 +38,69 @@ export class CatalogClientWrapper implements CatalogApi { this.identityApi = options.identityApi; } - async getLocationById(id: String): Promise { - const token = await this.identityApi.getIdToken(); - return await this.client.getLocationById(id, { token }); + private async injectToken(context?: ApiContext) { + const result: ApiContext = context ?? {}; + // Inject identity token if not provided + if (!result.token) { + result.token = await this.identityApi.getIdToken(); + } + return result; + } + + async getLocationById( + id: String, + context?: ApiContext, + ): Promise { + return await this.client.getLocationById( + id, + await this.injectToken(context), + ); } async getEntities( request?: CatalogEntitiesRequest, + context?: ApiContext, ): Promise> { - const token = await this.identityApi.getIdToken(); - return await this.client.getEntities(request, { token }); + return await this.client.getEntities( + request, + await this.injectToken(context), + ); } - async getEntityByName(compoundName: EntityName): Promise { - const token = await this.identityApi.getIdToken(); - return await this.client.getEntityByName(compoundName, { token }); + async getEntityByName( + compoundName: EntityName, + context?: ApiContext, + ): Promise { + return await this.client.getEntityByName( + compoundName, + await this.injectToken(context), + ); } - async addLocation(request: AddLocationRequest): Promise { - const token = await this.identityApi.getIdToken(); - return await this.client.addLocation(request, { token }); + async addLocation( + request: AddLocationRequest, + context?: ApiContext, + ): Promise { + return await this.client.addLocation( + request, + await this.injectToken(context), + ); } - async getLocationByEntity(entity: Entity): Promise { - const token = await this.identityApi.getIdToken(); - return await this.client.getLocationByEntity(entity, { token }); + async getLocationByEntity( + entity: Entity, + context?: ApiContext, + ): Promise { + return await this.client.getLocationByEntity( + entity, + await this.injectToken(context), + ); } - async removeEntityByUid(uid: string): Promise { - const token = await this.identityApi.getIdToken(); - return await this.client.removeEntityByUid(uid, { token }); + async removeEntityByUid(uid: string, context?: ApiContext): Promise { + return await this.client.removeEntityByUid( + uid, + await this.injectToken(context), + ); } } diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 55a9f29372..f4f121f0e6 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -27,7 +27,6 @@ import { catalogRouteRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { catalogRouteRef, entityRouteRef } from './routes'; import { CatalogClientWrapper } from './CatalogClientWrapper'; export const catalogPlugin = createPlugin({ diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index ed8b28552d..9ab9b17483 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -15,13 +15,12 @@ */ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { CatalogClient } from '@backstage/catalog-client'; +import { ApiContext, CatalogClient } from '@backstage/catalog-client'; import { ConflictError, NotFoundError, PluginEndpointDiscovery, } from '@backstage/backend-common'; -import { CatalogEntityRequestOptions } from './types'; /** * A catalog client tailored for reading out entity data from the catalog. @@ -42,9 +41,8 @@ export class CatalogEntityClient { */ async findTemplate( templateName: string, - options?: CatalogEntityRequestOptions, + context?: ApiContext, ): Promise { - const token = options?.token; const { items: templates } = (await this.catalogClient.getEntities( { filter: { @@ -52,7 +50,7 @@ export class CatalogEntityClient { 'metadata.name': templateName, }, }, - { token }, + context, )) as { items: TemplateEntityV1alpha1[] }; if (templates.length !== 1) { diff --git a/plugins/scaffolder-backend/src/lib/catalog/types.ts b/plugins/scaffolder-backend/src/lib/catalog/types.ts deleted file mode 100644 index 188efbd9d0..0000000000 --- a/plugins/scaffolder-backend/src/lib/catalog/types.ts +++ /dev/null @@ -1,32 +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. - */ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export type CatalogEntityRequestOptions = { - token: string | undefined; -}; From 72b96e880c7c46333123ecfa3f2e415089c4a66f Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Mon, 1 Feb 2021 15:46:31 +0100 Subject: [PATCH 031/187] Allow the registration of optional locations via the catalog-client --- .changeset/poor-cougars-remember.md | 5 +++++ packages/catalog-client/src/CatalogClient.ts | 3 ++- packages/catalog-client/src/types.ts | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/poor-cougars-remember.md diff --git a/.changeset/poor-cougars-remember.md b/.changeset/poor-cougars-remember.md new file mode 100644 index 0000000000..e667c8e69b --- /dev/null +++ b/.changeset/poor-cougars-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': patch +--- + +Add the `presence` argument to the `CatalogApi` to be able to register optional locations. diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 362b1e71da..c3d9749161 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -75,6 +75,7 @@ export class CatalogClient implements CatalogApi { type = 'url', target, dryRun, + presence, }: AddLocationRequest): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ @@ -85,7 +86,7 @@ export class CatalogClient implements CatalogApi { 'Content-Type': 'application/json', }, method: 'POST', - body: JSON.stringify({ type, target }), + body: JSON.stringify({ type, target, presence }), }, ); diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index e72317d444..52b001a2bc 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -40,6 +40,7 @@ export type AddLocationRequest = { type?: string; target: string; dryRun?: boolean; + presence?: 'optional' | 'required'; }; export type AddLocationResponse = { From 19d354c78e65c3df6b0353050889b7a6c259e0ab Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Mon, 1 Feb 2021 15:47:30 +0100 Subject: [PATCH 032/187] Make EntityRefLink a React.forwardRef in order to use it as root component in other components like ListItem --- .changeset/shy-squids-eat.md | 5 ++ .../EntityRefLink/EntityRefLink.tsx | 74 +++++++++---------- 2 files changed, 42 insertions(+), 37 deletions(-) create mode 100644 .changeset/shy-squids-eat.md diff --git a/.changeset/shy-squids-eat.md b/.changeset/shy-squids-eat.md new file mode 100644 index 0000000000..ee652778a7 --- /dev/null +++ b/.changeset/shy-squids-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Make `EntityRefLink` a `React.forwardRef` in order to use it as root component in other components like `ListItem`. diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index ae6f807275..8aa890086e 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -15,13 +15,12 @@ */ import { Entity, - EntityName, ENTITY_DEFAULT_NAMESPACE, + EntityName, } from '@backstage/catalog-model'; -import { Link } from '@material-ui/core'; +import { Link } from '@backstage/core'; import React from 'react'; import { generatePath } from 'react-router'; -import { Link as RouterLink } from 'react-router-dom'; import { entityRoute } from '../../routes'; import { formatEntityRefTitle } from './format'; @@ -31,41 +30,42 @@ type EntityRefLinkProps = { children?: React.ReactNode; }; -export const EntityRefLink = ({ - entityRef, - defaultKind, - children, -}: EntityRefLinkProps) => { - let kind; - let namespace; - let name; +export const EntityRefLink = React.forwardRef( + (props, ref) => { + const { entityRef, defaultKind, children } = props; - if ('metadata' in entityRef) { - kind = entityRef.kind; - namespace = entityRef.metadata.namespace; - name = entityRef.metadata.name; - } else { - kind = entityRef.kind; - namespace = entityRef.namespace; - name = entityRef.name; - } + let kind; + let namespace; + let name; - kind = kind.toLowerCase(); + if ('metadata' in entityRef) { + kind = entityRef.kind; + namespace = entityRef.metadata.namespace; + name = entityRef.metadata.name; + } else { + kind = entityRef.kind; + namespace = entityRef.namespace; + name = entityRef.name; + } - const routeParams = { - kind, - namespace: namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE, - name, - }; + kind = kind.toLowerCase(); - // TODO: Use useRouteRef here to generate the path - return ( - - {children} - {!children && formatEntityRefTitle(entityRef, { defaultKind })} - - ); -}; + const routeParams = { + kind, + namespace: namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE, + name, + }; + + // TODO: Use useRouteRef here to generate the path + return ( + + {children} + {!children && formatEntityRefTitle(entityRef, { defaultKind })} + + ); + }, +); From 68dd79d83a2fece0d3a398dc4a25d00ef33b1e1f Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Mon, 1 Feb 2021 15:43:50 +0100 Subject: [PATCH 033/187] Rework the user flow of the catalog-import plugin --- .changeset/late-books-greet.md | 20 + packages/app/src/App.tsx | 5 +- plugins/catalog-import/README.md | 89 +++- plugins/catalog-import/dev/index.tsx | 212 ++++++++- .../docs/catalog-import-screenshot.png | Bin 0 -> 628108 bytes plugins/catalog-import/package.json | 4 + .../src/api/CatalogImportApi.ts | 39 +- .../src/api/CatalogImportClient.test.ts | 344 +++++++++++++-- .../src/api/CatalogImportClient.ts | 195 ++++++--- plugins/catalog-import/src/api/GitHub.ts | 42 ++ .../src/assets/catalog-import-screenshot.png | Bin 22602 -> 0 bytes .../src/components/Buttons/index.tsx | 69 +++ .../src/components/ComponentConfigDisplay.tsx | 200 --------- .../EntityListComponent.tsx | 163 +++++++ .../EntityListComponent/index.ts} | 8 +- .../components/ImportComponentForm.test.tsx | 90 ---- .../src/components/ImportComponentForm.tsx | 154 ------- .../components/ImportComponentPage.test.tsx | 248 ++--------- .../src/components/ImportComponentPage.tsx | 154 +++---- .../src/components/ImportFinished.tsx | 58 --- .../src/components/ImportStepper.tsx | 42 -- .../ImportStepper/ImportStepper.tsx | 84 ++++ .../src/components/ImportStepper/defaults.tsx | 309 ++++++++++++++ .../ImportStepper/index.ts} | 15 +- .../catalog-import/src/components/Router.tsx | 8 +- .../StepFinishImportLocation.tsx | 65 +++ .../StepFinishImportLocation/index.ts} | 16 +- .../StepInitAnalyzeUrl.test.tsx | 401 ++++++++++++++++++ .../StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx | 158 +++++++ .../components/StepInitAnalyzeUrl/index.ts | 17 + .../AutocompleteTextField.tsx | 100 +++++ .../PreparePullRequestForm.test.tsx | 113 +++++ .../PreparePullRequestForm.tsx | 67 +++ .../PreviewCatalogInfoComponent.test.tsx | 101 +++++ .../PreviewCatalogInfoComponent.tsx | 56 +++ .../PreviewPullRequestComponent.test.tsx | 83 ++++ .../PreviewPullRequestComponent.tsx | 40 ++ .../StepPrepareCreatePullRequest.test.tsx | 288 +++++++++++++ .../StepPrepareCreatePullRequest.tsx | 247 +++++++++++ .../StepPrepareCreatePullRequest/index.ts | 21 + .../StepPrepareSelectLocations.test.tsx | 202 +++++++++ .../StepPrepareSelectLocations.tsx | 124 ++++++ .../StepPrepareSelectLocations/index.ts | 17 + .../StepReviewLocation/StepReviewLocation.tsx | 134 ++++++ .../components/StepReviewLocation/index.ts | 17 + .../catalog-import/src/components/index.ts | 20 + .../src/components/useImportState.test.tsx | 358 ++++++++++++++++ .../src/components/useImportState.ts | 262 ++++++++++++ plugins/catalog-import/src/index.ts | 1 + .../mocks/analyze-location-POST-response.json | 22 - .../src/mocks/locations-POST-response.json | 39 -- plugins/catalog-import/src/plugin.ts | 18 +- plugins/catalog-import/src/setupTests.ts | 1 - .../catalog-import/src/util/useGithubRepos.ts | 121 ------ .../catalog-import/src/util/validate.test.ts | 33 -- 55 files changed, 4443 insertions(+), 1251 deletions(-) create mode 100644 .changeset/late-books-greet.md create mode 100644 plugins/catalog-import/docs/catalog-import-screenshot.png create mode 100644 plugins/catalog-import/src/api/GitHub.ts delete mode 100644 plugins/catalog-import/src/assets/catalog-import-screenshot.png create mode 100644 plugins/catalog-import/src/components/Buttons/index.tsx delete mode 100644 plugins/catalog-import/src/components/ComponentConfigDisplay.tsx create mode 100644 plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx rename plugins/catalog-import/src/{util/validate.ts => components/EntityListComponent/index.ts} (72%) delete mode 100644 plugins/catalog-import/src/components/ImportComponentForm.test.tsx delete mode 100644 plugins/catalog-import/src/components/ImportComponentForm.tsx delete mode 100644 plugins/catalog-import/src/components/ImportFinished.tsx delete mode 100644 plugins/catalog-import/src/components/ImportStepper.tsx create mode 100644 plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx create mode 100644 plugins/catalog-import/src/components/ImportStepper/defaults.tsx rename plugins/catalog-import/src/{util/types.ts => components/ImportStepper/index.ts} (64%) create mode 100644 plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx rename plugins/catalog-import/src/{util/urls.ts => components/StepFinishImportLocation/index.ts} (62%) create mode 100644 plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx create mode 100644 plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx create mode 100644 plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts create mode 100644 plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts create mode 100644 plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx create mode 100644 plugins/catalog-import/src/components/StepReviewLocation/index.ts create mode 100644 plugins/catalog-import/src/components/index.ts create mode 100644 plugins/catalog-import/src/components/useImportState.test.tsx create mode 100644 plugins/catalog-import/src/components/useImportState.ts delete mode 100644 plugins/catalog-import/src/mocks/analyze-location-POST-response.json delete mode 100644 plugins/catalog-import/src/mocks/locations-POST-response.json delete mode 100644 plugins/catalog-import/src/util/useGithubRepos.ts delete mode 100644 plugins/catalog-import/src/util/validate.test.ts diff --git a/.changeset/late-books-greet.md b/.changeset/late-books-greet.md new file mode 100644 index 0000000000..9beff968e8 --- /dev/null +++ b/.changeset/late-books-greet.md @@ -0,0 +1,20 @@ +--- +'example-app': patch +'@backstage/plugin-catalog-import': minor +--- + +The plugin has been refactored and is now based on a configurable state machine of 'analyze', 'prepare', 'review' & 'finish'. +Depending on the outcome of the 'analyze' stage, different flows are selected ('single-location', 'multiple-locations', 'no-location'). +Each flow can define it's own components that guide the user. + +During the refactoring, the `catalogRouteRef` property of the `ImportComponentRouter` has been removed, so the `App.tsx` of the backstage apps need to be updated: + +```diff +// packages/app/src/App.tsx + + } ++ element={} + /> +``` diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 1b76773979..5910d4674c 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -69,10 +69,7 @@ const catalogRouteRef = createRouteRef({ const routes = ( - } - /> + } /> } diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index 1dc0d7cf89..c9583eb874 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -1,21 +1,88 @@ -# Catalog import plugin +# Catalog Import -Welcome to the catalog-import plugin! +The Catalog Import Plugin provides a wizard to onboard projects with existing `catalog-info.yaml` files. +It also assists by creating pull requests in repositories where no `catalog-info.yaml` exists. -This plugin allows you to bootstrap a component-config YAML file for your repository and open a pull request to add it. +![Catalog Import Plugin](./docs/catalog-import-screenshot.png) -When installed it is accessible on [localhost:3000/catalog-import](localhost:3000/catalog-import). +Current features: - +- Import `catalog-info.yaml` files from a URL in a repository of one of the supported Git integrations (example `https://github.com/backstage/backstage/catalog.info`). +- _[GitHub only]_ Search for all `catalog-info.yaml` files in a Git repository (example: `https://github.com/backstage/backstage`). +- _[GitHub only]_ Analyze a repository, generate a Component entity, and create a Pull Request to onboard the repository. -## Running +Some features are not yet available for all supported Git providers. -Just run the backstage. +## Getting Started -``` -yarn start && yarn --cwd packages/backend start +1. Install the Catalog Import Plugin: + +```bash +# packages/app + +yarn add @backstage/plugin-catalog-import ``` -## Usage +2. Add the plugin to the app: -Pretty straightforward, navigate to [localhost:3000/catalog-import](localhost:3000/catalog-import) and enter your repo's URL. +```ts +// packages/app/src/plugins.ts + +export { plugin as CatalogImportPlugin } from '@backstage/plugin-catalog-import'; +``` + +3. Register the `ImportComponentRouter` at the `/catalog-import` path: + +```tsx +// packages/app/src/App.tsx + +import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; + +} />; +``` + +## Customizations + +### Disable the creation of Pull Requests + +The pull request feature can be disabled by options that are passed to the `ImportComponentRouter`: + +```tsx +// packages/app/src/App.tsx + + + } +/> +``` + +### Customize the title and body of the Pull Request + +The pull request form is filled with a default title and body. +This can be configured by options that are passed to the `ImportComponentRouter`: + +```tsx +// packages/app/src/App.tsx + + ({ + title: 'My title', + body: 'My **markdown** body', + }), + }, + }} + /> + } +/> +``` + +## Development + +Use `yarn start` to run a [development version](./dev/index.tsx) of the plugin that can be used to validate each flow with mocked data. diff --git a/plugins/catalog-import/dev/index.tsx b/plugins/catalog-import/dev/index.tsx index c9b46a7ed4..fc0a4c6c1b 100644 --- a/plugins/catalog-import/dev/index.tsx +++ b/plugins/catalog-import/dev/index.tsx @@ -14,7 +14,215 @@ * limitations under the License. */ +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity, EntityName } from '@backstage/catalog-model'; +import { Content, Header, Page } from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; -import { catalogImportPlugin } from '../src/plugin'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { + AnalyzeResult, + CatalogImportApi, + catalogImportApiRef, + ImportStepper, +} from '../src'; +import { ImportComponentPage } from '../src/components/ImportComponentPage'; -createDevApp().registerPlugin(catalogImportPlugin).render(); +const getEntityNames = (url: string): EntityName[] => [ + { + kind: 'Component', + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'component-a', + }, + { + kind: 'API', + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'api-a', + }, +]; + +const getEntities = (url: string): Entity[] => [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'component-a', + }, + }, + { + apiVersion: '1', + kind: 'API', + metadata: { + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'api-a', + }, + }, +]; + +createDevApp() + .registerApi({ + api: catalogApiRef, + deps: {}, + factory: () => + ({ + getEntities: async () => { + await new Promise(r => setTimeout(r, 1000)); + + return { + items: [ + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'group-a', + namespace: 'default', + }, + spec: { + profile: { + displayName: 'Group A', + }, + }, + }, + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'group-b', + namespace: 'default', + }, + spec: { + profile: { + displayName: 'Group B', + }, + }, + }, + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'group-a', + namespace: 'other', + }, + spec: { + profile: { + displayName: 'Group A', + }, + }, + }, + ] as Entity[], + }; + }, + + addLocation: async location => { + await new Promise(r => setTimeout(r, 1000)); + + return { + location, + entities: getEntities(location.target), + }; + }, + } as CatalogApi), + }) + .registerApi({ + api: catalogImportApiRef, + deps: {}, + factory: () => + ({ + analyzeUrl: async (url: string): Promise => { + await new Promise(r => setTimeout(r, 500)); + + switch (url) { + case 'https://0': + return { + type: 'repository', + url, + integrationType: 'github', + generatedEntities: getEntities(url), + }; + + case 'https://1': + case 'https://2/catalog-info.yaml': + case 'https://2/folder-a/catalog-info.yaml': + case 'https://2/folder-b/catalog-info.yaml': + return { + type: 'locations', + locations: [ + { + target: url.includes('/catalog-info.yaml') + ? url + : `${url}/catalog-info.yaml`, + entities: getEntityNames(url), + }, + ], + }; + + case 'https://2': { + const urls = [ + `${url}/catalog-info.yaml`, + `${url}/folder-a/catalog-info.yaml`, + `${url}/folder-b/catalog-info.yaml`, + ]; + + return { + type: 'locations', + locations: urls.map(u => ({ + target: u, + entities: getEntityNames(u), + })), + }; + } + + default: + throw new Error(`Invalid url ${url}`); + } + }, + + submitPullRequest: async ({ + repositoryUrl, + }): Promise<{ + link: string; + location: string; + }> => { + await new Promise(r => setTimeout(r, 2500)); + + return { + link: `${repositoryUrl}/pulls/1`, + location: `${repositoryUrl}/blob/catalog-info.yaml`, + }; + }, + } as CatalogImportApi), + }) + .addPage({ + title: 'Catalog Import', + element: , + }) + .addPage({ + title: 'Catalog Import 2', + element: ( + +
+ + + + + + + + + + + + + + + + + + + + + ), + }) + .render(); diff --git a/plugins/catalog-import/docs/catalog-import-screenshot.png b/plugins/catalog-import/docs/catalog-import-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c457d9c3de281499569e50167516d5915e15c2f1 GIT binary patch literal 628108 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μ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
972F9Y7QNfT4|)pm;2==%~oUKY_wQ+$na+m^QA? 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�AaT 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

g4~lXsv3Gd2j-QT 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 P;6I6LvR6~Yblm({ id: 'plugin.catalog-import.service', description: 'Used by the catalog import plugin to make requests', }); +// result of the analyze state +export type AnalyzeResult = + | { + type: 'locations'; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + generatedEntities: Entity[]; + }; + export interface CatalogImportApi { - submitPrToRepo(options: { - owner: string; - repo: string; + analyzeUrl(url: string): Promise; + + submitPullRequest(options: { + repositoryUrl: string; + integrationType: string; fileContent: string; - githubIntegrationConfig: GitHubIntegrationConfig; + title: string; + body: string; }): Promise<{ link: string; location: string }>; - checkForExistingCatalogInfo(options: { - owner: string; - repo: string; - githubIntegrationConfig: GitHubIntegrationConfig; - }): Promise<{ exists: boolean; url?: string }>; - createRepositoryLocation(options: { location: string }): Promise; - generateEntityDefinitions(options: { - repo: string; - }): Promise; } diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 0e0cf4b323..cefe74e8ba 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -14,51 +14,317 @@ * limitations under the License. */ -import { CatalogImportClient } from './CatalogImportClient'; +const octokit = { + repos: { + get: () => Promise.resolve({ data: { default_branch: 'main' } }), + createOrUpdateFileContents: jest.fn().mockImplementation(async () => {}), + }, + search: { + code: jest.fn(), + }, + git: { + getRef: async () => ({ + data: { object: { sha: 'any' } }, + }), + createRef: jest.fn().mockImplementation(async () => {}), + }, + pulls: { + create: jest.fn().mockImplementation(async () => ({ + data: { + html_url: 'http://pull/request/0', + }, + })), + }, +}; -jest.mock('@octokit/rest', () => ({ - Octokit: jest.fn().mockImplementation(() => { - return { - repos: { - get: () => - Promise.resolve({ - data: { - default_branch: 'main', - }, - }), - }, - search: { - code: () => - Promise.resolve({ - data: { - total_count: 2, - items: [ - { path: 'simple/path/catalog-info.yaml' }, - { path: 'co/mple/x/path/catalog-info.yaml' }, - { path: 'catalog-info.yaml' }, - ], - }, - }), - }, - }; - }), +jest.doMock('@octokit/rest', () => { + class Octokit { + constructor() { + return octokit; + } + } + return { Octokit }; +}); + +// Mock the value to control which integrations are activated +jest.mock('./GitHub', () => ({ + getGithubIntegrationConfig: jest.fn(), })); +import { ConfigReader, OAuthApi, UrlPatternDiscovery } from '@backstage/core'; +import { GitHubIntegrationConfig } from '@backstage/integration'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { msw } from '@backstage/test-utils'; +import { Octokit } from '@octokit/rest'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { CatalogImportClient } from './CatalogImportClient'; +import { getGithubIntegrationConfig } from './GitHub'; + +const server = setupServer(); + describe('CatalogImportClient', () => { - describe('checkForExistingCatalogInfo', () => { - const cic = new CatalogImportClient({ - discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, - githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token') }, - configApi: {} as any, + msw.setupDefaultHandlers(server); + + const mockBaseUrl = 'http://backstage:9191/api/catalog'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + + const githubAuthApi: jest.Mocked = { + getAccessToken: jest.fn(), + }; + + const configApi = new ConfigReader({}); + + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + addLocation: jest.fn(), + getEntityByName: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + }; + + let catalogImportClient: CatalogImportClient; + let getGithubIntegrationConfigFn: jest.Mock; + + beforeEach(() => { + catalogImportClient = new CatalogImportClient({ + discoveryApi, + githubAuthApi, + configApi, + catalogApi, }); - it('should return the closest-to-root catalog-info from multiple responses', async () => { - const respo = await cic.checkForExistingCatalogInfo({ - owner: 'test-user', - repo: 'rest-repo', - githubIntegrationConfig: { host: 'https://github.com' }, + + getGithubIntegrationConfigFn = getGithubIntegrationConfig as jest.Mock; + getGithubIntegrationConfigFn.mockReset(); + }); + + describe('analyzeUrl', () => { + it('should add yaml location', async () => { + catalogApi.addLocation.mockResolvedValueOnce({ + location: { + id: 'id-0', + type: 'url', + target: 'http://example.com/folder/catalog-info.yaml', + }, + entities: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'my-entity', + namespace: 'my-namespace', + }, + }, + ], + }); + + await expect( + catalogImportClient.analyzeUrl( + 'http://example.com/folder/catalog-info.yaml', + ), + ).resolves.toEqual({ + locations: [ + { + entities: [ + { + kind: 'Component', + name: 'my-entity', + namespace: 'my-namespace', + }, + ], + target: 'http://example.com/folder/catalog-info.yaml', + }, + ], + type: 'locations', + }); + + expect(catalogApi.addLocation).toBeCalledTimes(1); + expect(catalogApi.addLocation.mock.calls[0][0]).toEqual({ + type: 'url', + target: 'http://example.com/folder/catalog-info.yaml', + dryRun: true, + }); + }); + + it('should ignore missing github integration', async () => { + await expect( + catalogImportClient.analyzeUrl( + 'https://github.com/backstage/backstage', + ), + ).rejects.toThrow(new Error('Invalid url')); + }); + + it('should find locations from github', async () => { + getGithubIntegrationConfigFn.mockReturnValue({ + repo: 'backstage', + owner: 'backstage', + githubIntegrationConfig: {} as GitHubIntegrationConfig, + }); + + ((new Octokit().search.code as any) as jest.Mock).mockResolvedValueOnce({ + data: { + total_count: 2, + items: [ + { path: 'simple/path/catalog-info.yaml' }, + { path: 'co/mple/x/path/catalog-info.yaml' }, + { path: 'catalog-info.yaml' }, + ], + }, + }); + + catalogApi.addLocation.mockImplementation(async ({ type, target }) => ({ + location: { + id: 'id-0', + type: type ?? 'url', + target, + }, + entities: [ + { + apiVersion: '1', + kind: 'k', + metadata: { + name: 'e', + namespace: 'n', + }, + }, + ], + })); + + await expect( + catalogImportClient.analyzeUrl( + 'https://github.com/backstage/backstage', + ), + ).resolves.toEqual({ + locations: [ + { + entities: [{ kind: 'k', name: 'e', namespace: 'n' }], + target: + 'https://github.com/backstage/backstage/blob/main/simple/path/catalog-info.yaml', + }, + { + entities: [{ kind: 'k', name: 'e', namespace: 'n' }], + target: + 'https://github.com/backstage/backstage/blob/main/co/mple/x/path/catalog-info.yaml', + }, + { + entities: [{ kind: 'k', name: 'e', namespace: 'n' }], + target: + 'https://github.com/backstage/backstage/blob/main/catalog-info.yaml', + }, + ], + type: 'locations', + }); + }); + + it('should find repository from github', async () => { + getGithubIntegrationConfigFn.mockReturnValue({ + repo: 'backstage', + owner: 'backstage', + githubIntegrationConfig: {} as GitHubIntegrationConfig, + }); + + ((new Octokit().search.code as any) as jest.Mock).mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }); + + server.use( + rest.post(`${mockBaseUrl}/analyze-location`, (req, res, ctx) => { + expect(req.body).toEqual({ + location: { + target: 'https://github.com/backstage/backstage', + type: 'url', + }, + }); + + return res( + ctx.json({ + generateEntities: [ + { + entity: { + kind: 'k', + metadata: { name: 'e', namespace: 'n' }, + }, + fields: [], + }, + ], + existingEntityFiles: [], + }), + ); + }), + ); + + await expect( + catalogImportClient.analyzeUrl( + 'https://github.com/backstage/backstage', + ), + ).resolves.toEqual({ + type: 'repository', + url: 'https://github.com/backstage/backstage', + integrationType: 'github', + generatedEntities: [ + { + kind: 'k', + metadata: { name: 'e', namespace: 'n' }, + }, + ], + }); + }); + }); + + describe('submitPullRequest', () => { + it('should create GitHub pull request', async () => { + getGithubIntegrationConfigFn.mockReturnValue({ + repo: 'backstage', + owner: 'backstage', + githubIntegrationConfig: { + host: 'github.com', + } as GitHubIntegrationConfig, + }); + + await expect( + catalogImportClient.submitPullRequest({ + integrationType: 'github', + repositoryUrl: 'https://github.com/backstage/backstage', + fileContent: 'some content', + title: 'A title', + body: 'A body', + }), + ).resolves.toEqual({ + link: 'http://pull/request/0', + location: + 'https://github.com/backstage/backstage/blob/main/catalog-info.yaml', + }); + + expect( + ((new Octokit().git.createRef as any) as jest.Mock).mock.calls[0][0], + ).toEqual({ + owner: 'backstage', + repo: 'backstage', + ref: 'refs/heads/backstage-integration', + sha: 'any', + }); + expect( + ((new Octokit().repos.createOrUpdateFileContents as any) as jest.Mock) + .mock.calls[0][0], + ).toEqual({ + owner: 'backstage', + repo: 'backstage', + path: 'catalog-info.yaml', + message: 'Add catalog-info.yaml config file', + content: 'c29tZSBjb250ZW50', + branch: 'backstage-integration', + }); + expect( + ((new Octokit().pulls.create as any) as jest.Mock).mock.calls[0][0], + ).toEqual({ + owner: 'backstage', + repo: 'backstage', + title: 'A title', + head: 'backstage-integration', + body: 'A body', + base: 'main', }); - expect(respo.exists).toBe(true); - expect(respo.url).toBe('blob/main/catalog-info.yaml'); }); }); }); diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index d1509c2105..1cdd726a9a 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -14,32 +14,117 @@ * limitations under the License. */ -import { Octokit } from '@octokit/rest'; -import { DiscoveryApi, OAuthApi, ConfigApi } from '@backstage/core'; -import { CatalogImportApi } from './CatalogImportApi'; -import { PartialEntity } from '../util/types'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity, EntityName } from '@backstage/catalog-model'; +import { ConfigApi, DiscoveryApi, OAuthApi } from '@backstage/core'; import { GitHubIntegrationConfig } from '@backstage/integration'; +import { Octokit } from '@octokit/rest'; +import fetch from 'cross-fetch'; +import { AnalyzeResult, CatalogImportApi } from './CatalogImportApi'; +import { getGithubIntegrationConfig } from './GitHub'; export class CatalogImportClient implements CatalogImportApi { private readonly discoveryApi: DiscoveryApi; private readonly githubAuthApi: OAuthApi; private readonly configApi: ConfigApi; + private readonly catalogApi: CatalogApi; constructor(options: { discoveryApi: DiscoveryApi; githubAuthApi: OAuthApi; configApi: ConfigApi; + catalogApi: CatalogApi; }) { this.discoveryApi = options.discoveryApi; this.githubAuthApi = options.githubAuthApi; this.configApi = options.configApi; + this.catalogApi = options.catalogApi; } - async generateEntityDefinitions({ + async analyzeUrl(url: string): Promise { + if (url.match(/\.ya?ml$/)) { + const location = await this.catalogApi.addLocation({ + type: 'url', + target: url, + dryRun: true, + }); + + return { + type: 'locations', + locations: [ + { + target: location.location.target, + entities: location.entities.map(e => ({ + kind: e.kind, + namespace: e.metadata.namespace ?? 'default', + name: e.metadata.name, + })), + }, + ], + }; + } + + const ghConfig = getGithubIntegrationConfig(this.configApi, url); + + if (ghConfig) { + // TODO: this could be part of the analyze-location endpoint + const locations = await this.checkGitHubForExistingCatalogInfo({ + ...ghConfig, + url, + }); + + if (locations.length > 0) { + return { + type: 'locations', + locations, + }; + } + + return { + type: 'repository', + integrationType: 'github', + url: url, + generatedEntities: await this.generateEntityDefinitions({ + repo: url, + }), + }; + } + + throw new Error('Invalid url'); + } + + async submitPullRequest({ + integrationType, + repositoryUrl, + fileContent, + title, + body, + }: { + repositoryUrl: string; + integrationType: string; + fileContent: string; + title: string; + body: string; + }): Promise<{ link: string; location: string }> { + const ghConfig = getGithubIntegrationConfig(this.configApi, repositoryUrl); + if (integrationType === 'github' && ghConfig) { + return await this.submitGitHubPrToRepo({ + ...ghConfig, + fileContent, + title, + body, + }); + } + + throw new Error('unimplemented!'); + } + + // TODO: this could be part of the catalog api + private async generateEntityDefinitions({ repo, }: { repo: string; - }): Promise { + }): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`, { @@ -64,41 +149,23 @@ export class CatalogImportClient implements CatalogImportApi { return payload.generateEntities.map((x: any) => x.entity); } - async createRepositoryLocation({ - location, - }: { - location: string; - }): Promise { - const response = await fetch( - `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, - { - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - body: JSON.stringify({ - type: 'url', - target: location, - presence: 'optional', - }), - }, - ); - if (!response.ok) { - throw new Error( - `Received http response ${response.status}: ${response.statusText}`, - ); - } - } - - async checkForExistingCatalogInfo({ + // TODO: this response should better be part of the analyze-locations response and scm-independent / implemented per scm + private async checkGitHubForExistingCatalogInfo({ + url, owner, repo, githubIntegrationConfig, }: { + url: string; owner: string; repo: string; githubIntegrationConfig: GitHubIntegrationConfig; - }): Promise<{ exists: boolean; url?: string }> { + }): Promise< + Array<{ + target: string; + entities: EntityName[]; + }> + > { const token = await this.githubAuthApi.getAccessToken(['repo']); const octo = new Octokit({ auth: token, @@ -122,28 +189,50 @@ export class CatalogImportClient implements CatalogImportApi { }); const defaultBranch = repoInformation.data.default_branch; - // Github search sorts returned values with 'best match' using 'multiple factors to boost the most relevant item', - // aka magic. - // Sorting to use the shortest item, closest to the repository root. - const catalogInfoItem = searchResult.data.items - .map(it => it.path) - .sort((a, b) => a.length - b.length)[0]; - return { - url: `blob/${defaultBranch}/${catalogInfoItem}`, - exists, - }; + return await Promise.all( + searchResult.data.items + .map( + i => `${url.replace(/[\/]*$/, '')}/blob/${defaultBranch}/${i.path}`, + ) + .map( + async i => + ({ + target: i, + entities: ( + await this.catalogApi.addLocation({ + type: 'url', + target: i, + dryRun: true, + }) + ).entities.map(e => ({ + kind: e.kind, + namespace: e.metadata.namespace ?? 'default', + name: e.metadata.name, + })), + } as { + target: string; + entities: EntityName[]; + }), + ), + ); } - return { exists }; + + return []; } - async submitPrToRepo({ + // TODO: extract this function and implement for non-github + private async submitGitHubPrToRepo({ owner, repo, + title, + body, fileContent, githubIntegrationConfig, }: { owner: string; repo: string; + title: string; + body: string; fileContent: string; githubIntegrationConfig: GitHubIntegrationConfig; }): Promise<{ link: string; location: string }> { @@ -212,23 +301,13 @@ export class CatalogImportClient implements CatalogImportApi { ); }); - const appTitle = - this.configApi.getOptionalString('app.title') ?? 'Backstage'; - const appBaseUrl = this.configApi.getString('app.baseUrl'); - - const prBody = `This pull request adds a **Backstage entity metadata file** \ -to this repository so that the component can be added to the \ -[${appTitle} software catalog](${appBaseUrl}).\n\nAfter this pull request is merged, \ -the component will become available.\n\nFor more information, read an \ -[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview).`; - const pullRequestResponse = await octo.pulls .create({ owner, repo, - title: `Add ${fileName} config file`, + title, head: branchName, - body: prBody, + body, base: repoData.data.default_branch, }) .catch(e => { diff --git a/plugins/catalog-import/src/api/GitHub.ts b/plugins/catalog-import/src/api/GitHub.ts new file mode 100644 index 0000000000..7805e8fb59 --- /dev/null +++ b/plugins/catalog-import/src/api/GitHub.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigApi } from '@backstage/core'; + +// TODO: (O5ten) Refactor into a core API instead of direct usage like this +// https://github.com/backstage/backstage/pull/3613#issuecomment-7408929430 +import { readGitHubIntegrationConfigs } from '@backstage/integration'; +import parseGitUrl from 'git-url-parse'; + +export const getGithubIntegrationConfig = ( + config: ConfigApi, + location: string, +) => { + const { name: repo, owner, resource: hostname } = parseGitUrl(location); + + const configs = readGitHubIntegrationConfigs( + config.getOptionalConfigArray('integrations.github') ?? [], + ); + const githubIntegrationConfig = configs.find(v => v.host === hostname); + if (!githubIntegrationConfig) { + return undefined; + } + return { + repo, + owner, + githubIntegrationConfig, + }; +}; diff --git a/plugins/catalog-import/src/assets/catalog-import-screenshot.png b/plugins/catalog-import/src/assets/catalog-import-screenshot.png deleted file mode 100644 index 7ba8ae9eb53c078010bf2ea461f015783a46dced..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22602 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 diff --git a/plugins/catalog-import/src/components/Buttons/index.tsx b/plugins/catalog-import/src/components/Buttons/index.tsx new file mode 100644 index 0000000000..18f968a32b --- /dev/null +++ b/plugins/catalog-import/src/components/Buttons/index.tsx @@ -0,0 +1,69 @@ +/* + * 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 { Button, CircularProgress } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { ComponentProps } from 'react'; + +const useStyles = makeStyles(theme => ({ + wrapper: { + marginTop: theme.spacing(1), + marginRight: theme.spacing(1), + position: 'relative', + }, + buttonProgress: { + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12, + }, + button: { + marginTop: theme.spacing(1), + marginRight: theme.spacing(1), + }, +})); + +export const NextButton = ( + props: ComponentProps & { loading?: boolean }, +) => { + const classes = useStyles(); + + return ( +
    +
    + ); +}; + +export const BackButton = (props: ComponentProps) => { + const classes = useStyles(); + + return ( + + ); +}; diff --git a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx deleted file mode 100644 index 9fb170c85c..0000000000 --- a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx +++ /dev/null @@ -1,200 +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 { - errorApiRef, - RouteRef, - StructuredMetadataTable, - useApi, -} from '@backstage/core'; -import { - entityRoute, - entityRouteParams, -} from '@backstage/plugin-catalog-react'; -import { - Button, - CircularProgress, - Divider, - Grid, - Link, - List, - ListItem, - Typography, -} from '@material-ui/core'; -import React, { useCallback, useState } from 'react'; -import { generatePath, resolvePath } from 'react-router'; -import { Link as RouterLink } from 'react-router-dom'; -import * as YAML from 'yaml'; -import { PartialEntity } from '../util/types'; -import { urlType } from '../util/urls'; -import { useGithubRepos } from '../util/useGithubRepos'; -import { ConfigSpec } from './ImportComponentPage'; - -const getEntityCatalogPath = ({ - entity, - catalogRouteRef, -}: { - entity: PartialEntity; - catalogRouteRef: RouteRef; -}) => { - const relativeEntityPathInsideCatalog = generatePath( - entityRoute.path, - entityRouteParams(entity as Entity), - ); - - const resolvedAbsolutePath = resolvePath( - relativeEntityPathInsideCatalog, - catalogRouteRef.path, - )?.pathname; - - return resolvedAbsolutePath; -}; - -type Props = { - nextStep: (options?: { reset: boolean }) => void; - configFile: ConfigSpec; - savePRLink: (PRLink: string) => void; - catalogRouteRef: RouteRef; -}; - -const ComponentConfigDisplay = ({ - nextStep, - configFile, - savePRLink, - catalogRouteRef, -}: Props) => { - const [errorOccurred, setErrorOccurred] = useState(false); - const [submitting, setSubmitting] = useState(false); - const errorApi = useApi(errorApiRef); - const { submitPrToRepo, addLocation } = useGithubRepos(); - const onNext = useCallback(async () => { - try { - setSubmitting(true); - if (urlType(configFile.location) === 'tree') { - const result = await submitPrToRepo(configFile); - savePRLink(result.link); - setSubmitting(false); - nextStep(); - } else { - addLocation(configFile.location); - setSubmitting(false); - nextStep(); - } - } catch (e) { - setErrorOccurred(true); - setSubmitting(false); - errorApi.post(e); - } - }, [submitPrToRepo, configFile, nextStep, savePRLink, errorApi, addLocation]); - - return ( - - {urlType(configFile.location) === 'tree' ? ( - - Following config object will be submitted in a pull request to the - repository{' '} - - {configFile.location} - {' '} - and added as a new location to the backend - - ) : ( - - Following config object will be added as a new location to the backend{' '} - - {configFile.location} - - - )} - - - {urlType(configFile.location) === 'tree' ? ( -
    {YAML.stringify(configFile.config)}
    - ) : ( - - {configFile.config.map((entity: any, index: number) => { - const entityPath = getEntityCatalogPath({ - entity, - catalogRouteRef, - }); - return ( - - - - {entityPath} - - ), - }} - /> - - {index < configFile.config.length - 1 && ( - - )} - - ); - })} - - )} -
    - - {submitting ? ( - - - - ) : null} - - - {errorOccurred ? ( - - ) : null} - - -
    - ); -}; - -export default ComponentConfigDisplay; diff --git a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx new file mode 100644 index 0000000000..f43699af64 --- /dev/null +++ b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx @@ -0,0 +1,163 @@ +/* + * 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 { Entity, EntityName } from '@backstage/catalog-model'; +import { + EntityRefLink, + formatEntityRefTitle, +} from '@backstage/plugin-catalog-react'; +import { + Collapse, + IconButton, + List, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import ApartmentIcon from '@material-ui/icons/Apartment'; +import CategoryIcon from '@material-ui/icons/Category'; +import ExpandLessIcon from '@material-ui/icons/ExpandLess'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import ExtensionIcon from '@material-ui/icons/Extension'; +import GroupIcon from '@material-ui/icons/Group'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import MemoryIcon from '@material-ui/icons/Memory'; +import PersonIcon from '@material-ui/icons/Person'; +import WorkIcon from '@material-ui/icons/Work'; +import React, { useState } from 'react'; + +const useStyles = makeStyles(theme => ({ + nested: { + paddingLeft: theme.spacing(4), + }, +})); + +function sortEntities(entities: Array) { + return entities.sort((a, b) => + formatEntityRefTitle(a).localeCompare(formatEntityRefTitle(b)), + ); +} + +function getEntityIcon(entity: { kind: string }): React.ReactElement { + switch (entity.kind.toLowerCase()) { + case 'api': + return ; + + case 'component': + return ; + + case 'domain': + return ; + + case 'location': + return ; + + case 'group': + return ; + + case 'system': + return ; + + case 'user': + return ; + + default: + return ; + } +} + +export const EntityListComponent = ({ + locations, + collapsed = false, + locationListItemIcon, + onItemClick, + firstListItem, + withLinks = false, +}: { + locations: Array<{ target: string; entities: (Entity | EntityName)[] }>; + locationListItemIcon: (target: string) => React.ReactElement; + collapsed?: boolean; + firstListItem?: React.ReactElement; + onItemClick?: (target: string) => void; + withLinks?: boolean; +}) => { + const classes = useStyles(); + + const [expandedUrls, setExpandedUrls] = useState([]); + + const handleClick = (url: string) => { + setExpandedUrls(urls => + urls.includes(url) ? urls.filter(u => u !== url) : urls.concat(url), + ); + }; + + return ( + + {firstListItem} + {locations.map(r => ( + + onItemClick?.call(this, r.target)} + > + {locationListItemIcon(r.target)} + + + + {collapsed && ( + + handleClick(r.target)}> + {expandedUrls.includes(r.target) ? ( + + ) : ( + + )} + + + )} + + + + + {sortEntities(r.entities).map(entity => ( + + {getEntityIcon(entity)} + + + ))} + + + + ))} + + ); +}; diff --git a/plugins/catalog-import/src/util/validate.ts b/plugins/catalog-import/src/components/EntityListComponent/index.ts similarity index 72% rename from plugins/catalog-import/src/util/validate.ts rename to plugins/catalog-import/src/components/EntityListComponent/index.ts index 056131aaae..fc0efc5481 100644 --- a/plugins/catalog-import/src/util/validate.ts +++ b/plugins/catalog-import/src/components/EntityListComponent/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 Spotify AB + * 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. @@ -14,8 +14,4 @@ * limitations under the License. */ -export const ComponentIdValidators = { - httpsValidator: (value: any) => - (typeof value === 'string' && value.match(/^https:\/\//) !== null) || - 'Must start with https://.', -}; +export { EntityListComponent } from './EntityListComponent'; diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx deleted file mode 100644 index 38b1065325..0000000000 --- a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx +++ /dev/null @@ -1,90 +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 { CatalogClient } from '@backstage/catalog-client'; -import { - ApiProvider, - ApiRegistry, - DiscoveryApi, - errorApiRef, -} from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import React from 'react'; -import { catalogImportApiRef, CatalogImportClient } from '../api'; -import { RegisterComponentForm } from './ImportComponentForm'; - -describe('', () => { - let apis: ApiRegistry; - - const mockErrorApi: jest.Mocked = { - post: jest.fn(), - error$: jest.fn(), - }; - - beforeEach(() => { - apis = ApiRegistry.from([ - [catalogApiRef, new CatalogClient({ discoveryApi: {} as DiscoveryApi })], - [ - catalogImportApiRef, - new CatalogImportClient({ - discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, - githubAuthApi: { - getAccessToken: (_, __) => Promise.resolve('token'), - }, - configApi: {} as any, - }), - ], - [errorApiRef, mockErrorApi], - ]); - }); - - async function renderSUT( - nextStep: () => void = () => {}, - saveConfig: () => void = () => {}, - ) { - return await renderInTestApp( - - - , - ); - } - - it('Renders without exploding', async () => { - await renderSUT(); - expect( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - ).toBeInTheDocument(); - }); - - it('Should have basic URL validation for input', async () => { - await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { target: { value: 'not a url' } }, - ); - }); - await waitFor(() => { - fireEvent.click(screen.getByText('Next')); - }); - expect(screen.getByText('Must start with https://.')).toBeInTheDocument(); - }); -}); diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx deleted file mode 100644 index a3d355b33f..0000000000 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ /dev/null @@ -1,154 +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 { errorApiRef, useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { BackstageTheme } from '@backstage/theme'; -import { - Button, - FormControl, - FormHelperText, - TextField, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; -import { useForm } from 'react-hook-form'; -import { useMountedState } from 'react-use'; -import { urlType } from '../util/urls'; -import { useGithubRepos } from '../util/useGithubRepos'; -import { ComponentIdValidators } from '../util/validate'; -import { ConfigSpec } from './ImportComponentPage'; - -const useStyles = makeStyles(theme => ({ - form: { - alignItems: 'flex-start', - display: 'flex', - flexFlow: 'column nowrap', - }, - submit: { - marginTop: theme.spacing(1), - }, -})); - -type Props = { - nextStep: () => void; - saveConfig: (configFile: ConfigSpec) => void; - repository: string; -}; - -export const RegisterComponentForm = ({ - nextStep, - saveConfig, - repository, -}: Props) => { - const { register, handleSubmit, errors, formState } = useForm({ - mode: 'onChange', - }); - const classes = useStyles(); - const hasErrors = !!errors.componentLocation; - const dirty = formState?.isDirty; - const catalogApi = useApi(catalogApiRef); - - const isMounted = useMountedState(); - const errorApi = useApi(errorApiRef); - const { - generateEntityDefinitions, - checkForExistingCatalogInfo, - } = useGithubRepos(); - - const onSubmit = async (formData: Record) => { - const { componentLocation: target } = formData; - async function saveCatalogFileConfig(target: string) { - const data = await catalogApi.addLocation({ target }); - saveConfig({ - type: 'file', - location: data.location.target, - config: data.entities, - }); - } - - async function trySaveRepositoryConfig(target: string) { - const existingCatalog = await checkForExistingCatalogInfo(target); - if (existingCatalog.exists) { - const targetUrl = target.endsWith('/') - ? `${target}${existingCatalog.url}` - : `${target}/${existingCatalog.url}`; - await saveCatalogFileConfig(targetUrl); - } else { - saveConfig({ - type: 'tree', - location: target, - config: await generateEntityDefinitions(target), - }); - } - } - - try { - if (!isMounted()) return; - const type = urlType(target); - if (type === 'tree') { - await trySaveRepositoryConfig(target); - } else { - await saveCatalogFileConfig(target); - } - nextStep(); - } catch (e) { - errorApi.post(e); - } - }; - - return ( -
    - - - - {errors.componentLocation && ( - - {errors.componentLocation.message} - - )} - - - -
    - ); -}; diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index f6ef07e1b2..0c0ce3b47b 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -13,254 +13,54 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { CatalogClient } from '@backstage/catalog-client'; import { ApiProvider, ApiRegistry, configApiRef, - errorApiRef, + ConfigReader, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { msw, renderInTestApp } from '@backstage/test-utils'; -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { act, render } from '@testing-library/react'; import React from 'react'; import { catalogImportApiRef, CatalogImportClient } from '../api'; import { ImportComponentPage } from './ImportComponentPage'; -let codeSearchMockResponse: () => Promise<{ - data: { - total_count: number; - items: Array<{ path: string }>; - }; -}>; - -let findGithubConfigMockResponse = () => ({ - host: 'test.localhost', - owner: 'someuser', -}); - -jest.mock('@backstage/integration', () => ({ - readGitHubIntegrationConfigs: () => ({ - find: findGithubConfigMockResponse, - }), -})); - -jest.mock('@octokit/rest', () => ({ - Octokit: jest.fn().mockImplementation(() => { - return { - repos: { - get: () => - Promise.resolve({ - data: { - default_branch: 'main', - }, - }), - }, - search: { - code: codeSearchMockResponse, - }, - }; - }), -})); - -const OUR_GITHUB_TEST_REPO = 'https://github.com/someuser/somerepo'; describe('', () => { - const server = setupServer(); - msw.setupDefaultHandlers(server); - - beforeEach(() => { - server.use( - rest.post('https://backend.localhost/locations', (_, res, ctx) => { - return res( - ctx.status(201), - ctx.json(require('../mocks/locations-POST-response.json')), - ); - }), - rest.post('https://backend.localhost/analyze-location', (_, res, ctx) => { - return res( - ctx.json(require('../mocks/analyze-location-POST-response.json')), - ); - }), - ); - }); - beforeAll(() => server.listen()); - afterEach(() => server.resetHandlers()); - afterAll(() => server.close()); - let apis: ApiRegistry; - const mockErrorApi: jest.Mocked = { - post: jest.fn(), - error$: jest.fn(), - }; - beforeEach(() => { - const getBaseUrl = () => Promise.resolve('https://backend.localhost'); - apis = ApiRegistry.from([ - [ - catalogApiRef, - new CatalogClient({ - discoveryApi: { getBaseUrl }, - }), - ], - [ + apis = ApiRegistry.with( + configApiRef, + new ConfigReader({ integrations: {} }), + ) + .with(catalogApiRef, new CatalogClient({ discoveryApi: {} as any })) + .with( catalogImportApiRef, new CatalogImportClient({ - discoveryApi: { getBaseUrl }, - githubAuthApi: { - getAccessToken: (_, __) => Promise.resolve('token'), - }, + discoveryApi: {} as any, + githubAuthApi: {} as any, configApi: {} as any, + catalogApi: {} as any, }), - ], - [ - configApiRef, - { - getOptional: () => 'Title', - getOptionalConfigArray: () => [], - has: () => true, - getConfig: () => ({ - has: () => true, - }), - }, - ], - [errorApiRef, mockErrorApi], - ]); + ); }); - async function renderSUT() { - return await renderInTestApp( - - - , - ); - } - - it('Should not explode on non-Github URLs', async () => { - findGithubConfigMockResponse = () => undefined!!; - await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { - target: { - value: 'https://test-git-provider.localhost/someuser/somerepo', - }, - }, - ); - }); - - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - const firstStepInput = screen.queryByPlaceholderText( - 'https://github.com/backstage/backstage', - ); - expect(firstStepInput).not.toBeInTheDocument(); - }); - }); - - it('Should offer direct file import from non-Github URLs', async () => { - findGithubConfigMockResponse = () => undefined!!; - await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { - target: { - value: - 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml', - }, - }, - ); - }); - - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - const firstStepInput = screen.queryByPlaceholderText( - 'https://github.com/backstage/backstage', - ); - expect(firstStepInput).not.toBeInTheDocument(); - }); - expect( - screen.getByText( - 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml', - ), - ).toBeInTheDocument(); - }); - - it('Should use found yaml file directly and not create a pull request if GitHub api returns one', async () => { - findGithubConfigMockResponse = () => ({ - host: 'test.localhost', - owner: 'someuser', - }); - codeSearchMockResponse = () => - Promise.resolve({ - data: { - total_count: 3, - items: [ - { path: 'simple/path/catalog-info.yaml' }, - { path: 'co/mple/x/path/catalog-info.yaml' }, - { path: 'catalog-info.yaml' }, - ], - }, - }); - await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { target: { value: OUR_GITHUB_TEST_REPO } }, - ); - }); - - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - expect( - screen.getByText( - 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', + it('renders without exploding', async () => { + await act(async () => { + const { getByText } = render( + wrapInTestApp( + + + , ), - ).toBeInTheDocument(); - - const pullReqText = screen.queryByText('pull request'); - expect(pullReqText).not.toBeInTheDocument(); - }); - }); - - it('Should indicate a pull request creation when no yaml file found in the repo', async () => { - findGithubConfigMockResponse = () => ({ - host: 'test.localhost', - owner: 'someuser', - }); - codeSearchMockResponse = () => - Promise.resolve({ - data: { - total_count: 0, - items: [], - }, - }); - const { container } = await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { target: { value: OUR_GITHUB_TEST_REPO } }, ); - }); - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - expect(screen.getByText(OUR_GITHUB_TEST_REPO)).toBeInTheDocument(); + expect( + await getByText('Start tracking your component in Backstage'), + ).toBeInTheDocument(); }); - const textNode = container - .querySelector('a[href="https://github.com/someuser/somerepo"]') - ?.closest('p'); - expect(textNode?.innerHTML).toContain( - 'Following config object will be submitted in a pull request to the repository', - ); - expect( - screen.queryByText( - 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', - ), - ).not.toBeInTheDocument(); }); }); diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx index e284e95456..d77a764d75 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -14,36 +14,29 @@ * limitations under the License. */ -import React, { useState } from 'react'; -import { Grid, Typography } from '@material-ui/core'; +import { Entity } from '@backstage/catalog-model'; import { + ConfigApi, + configApiRef, + Content, + ContentHeader, + Header, InfoCard, Page, - Content, - Header, SupportButton, - ContentHeader, - RouteRef, useApi, - configApiRef, - ConfigApi, } from '@backstage/core'; -import { RegisterComponentForm } from './ImportComponentForm'; -import ImportStepper from './ImportStepper'; -import ComponentConfigDisplay from './ComponentConfigDisplay'; -import { ImportFinished } from './ImportFinished'; -import { PartialEntity } from '../util/types'; +import { Grid, Typography } from '@material-ui/core'; +import React from 'react'; +import { ImportStepper } from './ImportStepper'; +import { StepperProviderOpts } from './ImportStepper/defaults'; export type ConfigSpec = { type: 'tree' | 'file'; location: string; - config: PartialEntity[]; + config: Entity[]; }; -function manifestGenerationAvailable(configApi: ConfigApi): boolean { - return configApi.has('integrations.github'); -} - function repositories(configApi: ConfigApi): string[] { const integrations = configApi.getConfig('integrations'); const repositories = []; @@ -63,25 +56,16 @@ function repositories(configApi: ConfigApi): string[] { } export const ImportComponentPage = ({ - catalogRouteRef, + opts, }: { - catalogRouteRef: RouteRef; + opts?: StepperProviderOpts; }) => { - const [activeStep, setActiveStep] = useState(0); - const [configFile, setConfigFile] = useState({ - type: 'tree', - location: '', - config: [], - }); - const [endLink, setEndLink] = useState(''); - const nextStep = (options?: { reset: boolean }) => { - setActiveStep(step => (options?.reset ? 0 : step + 1)); - }; - const configApi = useApi(configApiRef); const appTitle = configApi.getOptional('app.title') || 'Backstage'; + const repos = repositories(configApi); const repositoryString = repos.join(', ').replace(/, (\w*)$/, ' or $1'); + return (
    @@ -92,9 +76,11 @@ export const ImportComponentPage = ({ software catalog. - - + + + - Ways to register an existing component + Enter the URL to your SCM repository to add it to {appTitle}. - - {manifestGenerationAvailable(configApi) && ( - - GitHub Repo - - If you already have code in a GitHub repository without - Backstage metadata file set up for it, enter the full URL to - your repo and a new pull request with a sample Backstage - metadata Entity File (catalog-info.yaml) will - be opened for you. - - - )} - {repos.length === 1 ? `${repos[0]} ` : ''} Repository & - Entity File + Link to an existing entity file + + + Example:{' '} + + https://github.com/backstage/backstage/blob/master/catalog-info.yaml + - If you've already created a {appTitle} metadata file and put it - in your {repositoryString} repository, you can enter the full - URL to that Entity File. + The wizard analyzes the file, previews the entities, and adds + them to the {appTitle} catalog. + {repos.length > 0 && ( + <> + + Link to a {repositoryString} repository + + + Example: https://github.com/backstage/backstage + + + The wizard discovery all catalog-info.yaml{' '} + files in the repository, previews the entities, and adds + them to the {appTitle} catalog. + + {!opts?.pullRequest?.disable && ( + + If no entities are found, the wizard will prepare a Pull + Request that adds an example{' '} + catalog-info.yaml and prepares the {appTitle}{' '} + catalog to load all entities as soon as the Pull Request + is merged. + + )} + + )} - - - - ), - }, - { - step: 'Review', - content: ( - - ), - }, - { - step: 'Finish', - content: ( - - ), - }, - ]} - activeStep={activeStep} - nextStep={nextStep} - /> - + + + diff --git a/plugins/catalog-import/src/components/ImportFinished.tsx b/plugins/catalog-import/src/components/ImportFinished.tsx deleted file mode 100644 index 9b0f8575a6..0000000000 --- a/plugins/catalog-import/src/components/ImportFinished.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { Alert } from '@material-ui/lab'; -import { Button, Grid, Link } from '@material-ui/core'; - -type Props = { - type: 'tree' | 'file'; - nextStep: (options?: { reset: boolean }) => void; - PRLink: string; -}; - -export const ImportFinished = ({ nextStep, PRLink, type }: Props) => { - return ( - - - - {type === 'tree' - ? 'Pull requests have been successfully opened. You can start again to import more repositories' - : 'Entity added to catalog successfully'} - - - - {type === 'tree' ? ( - - View pull request on GitHub - - ) : null} - - - - ); -}; diff --git a/plugins/catalog-import/src/components/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper.tsx deleted file mode 100644 index 03752fa7d4..0000000000 --- a/plugins/catalog-import/src/components/ImportStepper.tsx +++ /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. - */ -import React from 'react'; -import Stepper from '@material-ui/core/Stepper'; -import Step from '@material-ui/core/Step'; -import StepLabel from '@material-ui/core/StepLabel'; -import { StepContent } from '@material-ui/core'; - -type Props = { - steps: { step: string; content: React.ReactNode }[]; - activeStep: number; - nextStep: (options?: { reset: boolean }) => void; -}; - -export default function ImportStepper({ steps, activeStep }: Props) { - return ( - - {steps.map(({ step }) => { - const stepProps: { completed?: boolean } = {}; - return ( - - {step} - {steps[activeStep].content} - - ); - })} - - ); -} diff --git a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx new file mode 100644 index 0000000000..791cf17965 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx @@ -0,0 +1,84 @@ +/* + * 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 { configApiRef, InfoCard, useApi } from '@backstage/core'; +import { Stepper } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { useMemo } from 'react'; +import { ImportFlows, ImportState, useImportState } from '../useImportState'; +import { + defaultGenerateStepper, + defaultStepper, + StepperProvider, + StepperProviderOpts, +} from './defaults'; + +const useStyles = makeStyles(() => ({ + stepperRoot: { + padding: 0, + }, +})); + +export const ImportStepper = ({ + initialUrl, + generateStepper = defaultGenerateStepper, + variant, + opts, +}: { + initialUrl?: string; + generateStepper?: ( + flow: ImportFlows, + defaults: StepperProvider, + ) => StepperProvider; + variant?: string; + opts?: StepperProviderOpts; +}) => { + const configApi = useApi(configApiRef); + const classes = useStyles(); + const state = useImportState({ initialUrl }); + + const states = useMemo( + () => generateStepper(state.activeFlow, defaultStepper), + [generateStepper, state.activeFlow], + ); + + return ( + + + {states.analyze( + state as Extract, + { apis: { configApi }, opts }, + )} + {states.prepare( + state as Extract, + { apis: { configApi }, opts }, + )} + {states.review( + state as Extract, + { apis: { configApi }, opts }, + )} + {states.finish( + state as Extract, + { apis: { configApi }, opts }, + )} + + + ); +}; diff --git a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx new file mode 100644 index 0000000000..954aeb20dd --- /dev/null +++ b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx @@ -0,0 +1,309 @@ +/* + * 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 { ConfigApi } from '@backstage/core'; +import { + Box, + Step, + StepContent, + StepLabel, + TextField, + Typography, +} from '@material-ui/core'; +import React from 'react'; +import { BackButton } from '../Buttons'; +import { StepFinishImportLocation } from '../StepFinishImportLocation'; +import { StepInitAnalyzeUrl } from '../StepInitAnalyzeUrl'; +import { + AutocompleteTextField, + StepPrepareCreatePullRequest, +} from '../StepPrepareCreatePullRequest'; +import { StepPrepareSelectLocations } from '../StepPrepareSelectLocations'; +import { StepReviewLocation } from '../StepReviewLocation'; +import { ImportFlows, ImportState } from '../useImportState'; + +export type StepperProviderOpts = { + pullRequest?: { + disable?: boolean; + preparePullRequest?: (apis: StepperApis) => { title: string; body: string }; + }; +}; + +type StepperApis = { + configApi: ConfigApi; +}; + +export type StepperProvider = { + analyze: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => React.ReactElement; + prepare: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => React.ReactElement; + review: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => React.ReactElement; + finish: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => React.ReactElement; +}; + +function defaultPreparePullRequest(apis: StepperApis) { + const appTitle = apis.configApi.getOptionalString('app.title') ?? 'Backstage'; + const appBaseUrl = apis.configApi.getString('app.baseUrl'); + + return { + title: 'Add catalog-info.yaml config file', + body: `This pull request adds a **Backstage entity metadata file** \ +to this repository so that the component can be added to the \ +[${appTitle} software catalog](${appBaseUrl}).\n\nAfter this pull request is merged, \ +the component will become available.\n\nFor more information, read an \ +[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview).`, + }; +} + +/** + * The default stepper generation function. + * + * Override this function to customize the import flow. Each flow should at + * least override the prepare operation. + * + * @param flow the name of the active flow + * @param defaults the default steps + */ +export function defaultGenerateStepper( + flow: ImportFlows, + defaults: StepperProvider, +): StepperProvider { + switch (flow) { + // the prepare step is skipped but the label of the step is updated + case 'single-location': + return { + ...defaults, + prepare: () => ( + + + Discovered Locations: 1 + + } + > + Select Locations + + + ), + }; + + // let the user select one or more of the discovered locations in the prepare step + case 'multiple-locations': + return { + ...defaults, + prepare: (state, opts) => { + if (state.analyzeResult.type !== 'locations') { + return defaults.prepare(state, opts); + } + + return ( + + + Discovered Locations: {state.analyzeResult.locations.length} + + } + > + Select Locations + + + + + + ); + }, + }; + + case 'no-location': + return { + ...defaults, + prepare: (state, opts) => { + if (state.analyzeResult.type !== 'repository') { + return defaults.prepare(state, opts); + } + + const { title, body } = ( + opts?.opts?.pullRequest?.preparePullRequest ?? + defaultPreparePullRequest + )(opts.apis); + + return ( + + Create Pull Request + + + ( + <> + + + Pull Request Details + + + + + + + + + + Entity Configuration + + + + + + + + )} + /> + + + ); + }, + }; + + default: + return defaults; + } +} + +export const defaultStepper: StepperProvider = { + analyze: (state, { opts }) => ( + + Select URL + + + + + ), + + prepare: state => ( + + Optional}> + Import Actions + + + + + + ), + + review: state => ( + + Review + + + + + ), + + finish: state => ( + + Finish + + + + + ), +}; diff --git a/plugins/catalog-import/src/util/types.ts b/plugins/catalog-import/src/components/ImportStepper/index.ts similarity index 64% rename from plugins/catalog-import/src/util/types.ts rename to plugins/catalog-import/src/components/ImportStepper/index.ts index f23649a2a9..8d205562a4 100644 --- a/plugins/catalog-import/src/util/types.ts +++ b/plugins/catalog-import/src/components/ImportStepper/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 Spotify AB + * 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. @@ -14,14 +14,5 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; - -export type RecursivePartial = { - [P in keyof T]?: T[P] extends (infer U)[] - ? RecursivePartial[] - : T[P] extends object - ? RecursivePartial - : T[P]; -}; - -export type PartialEntity = RecursivePartial; +export { ImportStepper } from './ImportStepper'; +export { defaultGenerateStepper } from './defaults'; diff --git a/plugins/catalog-import/src/components/Router.tsx b/plugins/catalog-import/src/components/Router.tsx index 5175f8f077..294ccfcb31 100644 --- a/plugins/catalog-import/src/components/Router.tsx +++ b/plugins/catalog-import/src/components/Router.tsx @@ -14,15 +14,13 @@ * limitations under the License. */ -import { RouteRef } from '@backstage/core'; import React from 'react'; import { Route, Routes } from 'react-router-dom'; import { ImportComponentPage } from './ImportComponentPage'; +import { StepperProviderOpts } from './ImportStepper/defaults'; -export const Router = ({ catalogRouteRef }: { catalogRouteRef: RouteRef }) => ( +export const Router = ({ options }: { options?: StepperProviderOpts }) => ( - } - /> + } /> ); diff --git a/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx new file mode 100644 index 0000000000..cc5eb9adef --- /dev/null +++ b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx @@ -0,0 +1,65 @@ +/* + * 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 { Link } from '@backstage/core'; +import { Grid, Typography } from '@material-ui/core'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import React from 'react'; +import { BackButton } from '../Buttons'; +import { EntityListComponent } from '../EntityListComponent'; +import { ReviewResult } from '../useImportState'; + +type Props = { + reviewResult: ReviewResult; + onReset: () => void; +}; + +export const StepFinishImportLocation = ({ reviewResult, onReset }: Props) => ( + <> + {reviewResult.type === 'repository' && ( + <> + + The following Pull Request has been opened:{' '} + + {reviewResult.pullRequest.url} + + + + + Your entities will be imported as soon as the Pull Request is merged. + + + )} + + + The following entities have been added to the catalog: + + + } + withLinks + /> + + + Register another + + +); diff --git a/plugins/catalog-import/src/util/urls.ts b/plugins/catalog-import/src/components/StepFinishImportLocation/index.ts similarity index 62% rename from plugins/catalog-import/src/util/urls.ts rename to plugins/catalog-import/src/components/StepFinishImportLocation/index.ts index c51dc0a999..5f3d9f425c 100644 --- a/plugins/catalog-import/src/util/urls.ts +++ b/plugins/catalog-import/src/components/StepFinishImportLocation/index.ts @@ -14,18 +14,4 @@ * limitations under the License. */ -import parseGitUrl from 'git-url-parse'; - -export type UrlType = 'file' | 'tree'; - -export function urlType(url: string): UrlType { - const { filepathtype, filepath } = parseGitUrl(url); - - if (filepathtype === 'tree' || filepathtype === 'file') { - return filepathtype; - } else if (filepath?.match(/\.ya?ml$/)) { - return 'file'; - } - - return 'tree'; -} +export { StepFinishImportLocation } from './StepFinishImportLocation'; diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx new file mode 100644 index 0000000000..bfb2434926 --- /dev/null +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx @@ -0,0 +1,401 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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, errorApiRef } from '@backstage/core'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AnalyzeResult, catalogImportApiRef } from '../../api/'; +import { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl'; + +describe('', () => { + const catalogImportApi: jest.Mocked = { + analyzeUrl: jest.fn(), + submitPullRequest: jest.fn(), + }; + + const errorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + const location = { + target: 'url', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + ], + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders without exploding', async () => { + const { getByRole } = render( + undefined} />, + { + wrapper: Wrapper, + }, + ); + + expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument(); + expect(getByRole('textbox', { name: /Repository/i })).toHaveValue(''); + }); + + it('should use default analysis url', async () => { + const { getByRole } = render( + undefined} + analysisUrl="https://default" + />, + { + wrapper: Wrapper, + }, + ); + + expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument(); + expect(getByRole('textbox', { name: /Repository/i })).toHaveValue( + 'https://default', + ); + }); + + it('should not analyze without url', async () => { + const onAnalysisFn = jest.fn(); + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(catalogImportApi.analyzeUrl).toBeCalledTimes(0); + expect(onAnalysisFn).toBeCalledTimes(0); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze invalid value', async () => { + const onAnalysisFn = jest.fn(); + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'http:/', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(catalogImportApi.analyzeUrl).toBeCalledTimes(0); + expect(onAnalysisFn).toBeCalledTimes(0); + expect(errorApi.post).toBeCalledTimes(0); + expect(getByText('Must start with https://.')).toBeInTheDocument(); + }); + + it('should analyze single location', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'locations', + locations: [location], + } as AnalyzeResult; + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(1); + expect(onAnalysisFn.mock.calls[0]).toMatchObject([ + 'single-location', + 'https://my-repository', + analyzeResult, + { prepareResult: analyzeResult }, + ]); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should analyze multiple locations', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'locations', + locations: [location, location], + } as AnalyzeResult; + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-1', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(1); + expect(onAnalysisFn.mock.calls[0]).toMatchObject([ + 'multiple-locations', + 'https://my-repository-1', + analyzeResult, + ]); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze with no locations', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'locations', + locations: [], + } as AnalyzeResult; + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-1', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText('There are no entities at this location'), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should analyze repository', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'repository', + url: 'https://my-repository-2', + integrationType: 'github', + generatedEntities: [ + { + apiVersion: '1', + kind: 'component', + metadata: { + name: 'component-a', + }, + }, + ], + } as AnalyzeResult; + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(1); + expect(onAnalysisFn.mock.calls[0]).toMatchObject([ + 'no-location', + 'https://my-repository-2', + analyzeResult, + ]); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze repository without entities', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'repository', + url: 'https://my-repository-2', + integrationType: 'github', + generatedEntities: [], + } as AnalyzeResult; + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText("Couldn't generate entities for your repository"), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze repository if disabled', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'repository', + url: 'https://my-repository-2', + integrationType: 'github', + generatedEntities: [ + { + apiVersion: '1', + kind: 'component', + metadata: { + name: 'component-a', + }, + }, + ], + } as AnalyzeResult; + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText("Couldn't generate entities for your repository"), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should report unknown type to the errorapi', async () => { + const onAnalysisFn = jest.fn(); + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(({ type: 'unknown' } as any) as AnalyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText( + 'Received unknown analysis result of type unknown. Please contact the support team.', + ), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(1); + expect(errorApi.post.mock.calls[0][0]).toMatchObject( + new Error( + 'Received unknown analysis result of type unknown. Please contact the support team.', + ), + ); + }); +}); diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx new file mode 100644 index 0000000000..2716c2b267 --- /dev/null +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx @@ -0,0 +1,158 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { FormHelperText, Grid, TextField } from '@material-ui/core'; +import React, { useCallback, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { NextButton } from '../Buttons'; +import { ImportFlows, PrepareResult } from '../useImportState'; + +type FormData = { + url: string; +}; + +type Props = { + onAnalysis: ( + flow: ImportFlows, + url: string, + result: AnalyzeResult, + opts?: { prepareResult?: PrepareResult }, + ) => void; + disablePullRequest?: boolean; + analysisUrl?: string; +}; + +/** + * A form that lets the user input an url and analyze it for existing locations or potential entities. + * + * @param onAnalysis is called when the analysis was successful + * @param analysisUrl a url that can be used as a default value + * @param disablePullRequest if true, repositories without entities will abort the wizard + */ +export const StepInitAnalyzeUrl = ({ + onAnalysis, + analysisUrl = '', + disablePullRequest = false, +}: Props) => { + const errorApi = useApi(errorApiRef); + const catalogImportApi = useApi(catalogImportApiRef); + + const { register, handleSubmit, errors, watch } = useForm({ + mode: 'onTouched', + defaultValues: { + url: analysisUrl, + }, + }); + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(undefined); + + const handleResult = useCallback( + async ({ url }: FormData) => { + setSubmitted(true); + + try { + const analysisResult = await catalogImportApi.analyzeUrl(url); + + switch (analysisResult.type) { + case 'repository': + if ( + !disablePullRequest && + analysisResult.generatedEntities.length > 0 + ) { + onAnalysis('no-location', url, analysisResult); + } else { + setError("Couldn't generate entities for your repository"); + setSubmitted(false); + } + break; + + case 'locations': { + if (analysisResult.locations.length === 1) { + onAnalysis('single-location', url, analysisResult, { + prepareResult: analysisResult, + }); + } else if (analysisResult.locations.length > 1) { + onAnalysis('multiple-locations', url, analysisResult); + } else { + setError('There are no entities at this location'); + setSubmitted(false); + } + break; + } + + default: { + const err = `Received unknown analysis result of type ${ + (analysisResult as any).type + }. Please contact the support team.`; + setError(err); + setSubmitted(false); + + errorApi.post(new Error(err)); + break; + } + } + } catch (e) { + setError(e.message); + setSubmitted(false); + } + }, + [catalogImportApi, disablePullRequest, errorApi, onAnalysis], + ); + + return ( +
    + + (typeof value === 'string' && + value.match(/^https:\/\//) !== null) || + 'Must start with https://.', + }, + })} + required + /> + + {errors.url && ( + {errors.url.message} + )} + + {error && {error}} + + + + Analyze + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts new file mode 100644 index 0000000000..5d36c104c3 --- /dev/null +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/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 { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl'; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx new file mode 100644 index 0000000000..780be10dcb --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx @@ -0,0 +1,100 @@ +/* + * 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 { CircularProgress, TextField } from '@material-ui/core'; +import { TextFieldProps } from '@material-ui/core/TextField/TextField'; +import { Autocomplete } from '@material-ui/lab'; +import React from 'react'; +import { + Control, + Controller, + FieldErrors, + ValidationRules, +} from 'react-hook-form'; + +type Props = { + name: TFieldValue; + options: string[]; + required?: boolean; + + control?: Control>; + errors?: FieldErrors>; + rules?: ValidationRules; + + loading?: boolean; + loadingText?: string; + + helperText?: React.ReactNode; + errorHelperText?: string; + + textFieldProps?: Omit; +}; + +export const AutocompleteTextField = ({ + name, + options, + required, + control, + errors, + rules, + loading = false, + loadingText, + helperText, + errorHelperText, + textFieldProps = {}, +}: Props) => { + return ( + ( + onChange(value || '')} + onBlur={onBlur} + value={value} + autoSelect + freeSolo + renderInput={params => ( + + {loading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + {...textFieldProps} + /> + )} + /> + )} + /> + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx new file mode 100644 index 0000000000..0bf7746e6a --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx @@ -0,0 +1,113 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { FormHelperText, TextField } from '@material-ui/core'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { PreparePullRequestForm } from './PreparePullRequestForm'; + +describe('', () => { + it('renders without exploding', async () => { + const onSubmitFn = jest.fn(); + + const { getByRole } = render( + ( + <> + + {' '} + + )} + onSubmit={onSubmitFn} + />, + ); + + await act(async () => { + userEvent.click(getByRole('button', { name: /submit/i })); + }); + + expect(onSubmitFn).toBeCalledTimes(1); + expect(onSubmitFn.mock.calls[0][0]).toMatchObject({ main: 'default' }); + }); + + it('should register a text field', async () => { + const onSubmitFn = jest.fn(); + + const { getByRole, getByLabelText } = render( + ( + <> + + + + )} + onSubmit={onSubmitFn} + />, + ); + + await act(async () => { + userEvent.clear(getByLabelText('Main Field')); + await userEvent.type(getByLabelText('Main Field'), 'My Text'); + userEvent.click(getByRole('button', { name: /submit/i })); + }); + + expect(onSubmitFn).toBeCalledTimes(1); + expect(onSubmitFn.mock.calls[0][0]).toMatchObject({ main: 'My Text' }); + }); + + it('registers required attribute', async () => { + const onSubmitFn = jest.fn(); + + const { queryByText, getByRole } = render( + ( + <> + + {errors.main && ( + + Error in required main field + + )} + {' '} + + )} + onSubmit={onSubmitFn} + />, + ); + + expect(queryByText('Error in required main field')).not.toBeInTheDocument(); + + await act(async () => { + userEvent.click(getByRole('button', { name: /submit/i })); + }); + + expect(onSubmitFn).not.toBeCalled(); + expect(queryByText('Error in required main field')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx new file mode 100644 index 0000000000..9d59f4e10e --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + SubmitHandler, + UnpackNestedValue, + useForm, + UseFormMethods, + UseFormOptions, +} from 'react-hook-form'; + +type Props> = Pick< + UseFormOptions, + 'defaultValues' +> & { + onSubmit: SubmitHandler; + + render: ( + props: Pick< + UseFormMethods, + 'errors' | 'register' | 'control' + > & { + values: UnpackNestedValue; + }, + ) => React.ReactNode; +}; + +/** + * A form wrapper that creates a form that is used to prepare a pull request. It + * hosts the form logic. + * + * @param defaultValues the default values of the form + * @param onSubmit a callback that is executed when the form is submitted + * (initiated by a button of type="submit") + * @param render render the form elements + */ +export const PreparePullRequestForm = < + TFieldValues extends Record +>({ + defaultValues, + onSubmit, + render, +}: Props) => { + const { handleSubmit, watch, control, register, errors } = useForm< + TFieldValues + >({ mode: 'onTouched', defaultValues }); + + return ( +
    + {render({ values: watch(), errors, register, control })} +
    + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx new file mode 100644 index 0000000000..5bc22dbd42 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx @@ -0,0 +1,101 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { makeStyles } from '@material-ui/core'; +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; + +const useStyles = makeStyles({ + displayNone: { + display: 'none', + }, +}); + +const entities: Entity[] = [ + { + apiVersion: '1', + kind: 'Kind', + metadata: { + name: 'name', + }, + }, + { + apiVersion: '1', + kind: 'Kind_2', + metadata: { + name: 'name', + }, + }, +]; + +describe('', () => { + it('renders without exploding', async () => { + const { getByText } = render( + , + ); + + const repositoryUrl = getByText('http://my-repository/a/catalog-info.yaml'); + const kindText = getByText('Kind_2'); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).toBeVisible(); + expect(kindText).toBeInTheDocument(); + expect(kindText).toBeVisible(); + }); + + it('renders card with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const repositoryUrl = getByText('http://my-repository/a/catalog-info.yaml'); + const kindText = getByText('Kind_2'); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).not.toBeVisible(); + expect(kindText).toBeInTheDocument(); + expect(kindText).not.toBeVisible(); + }); + + it('renders with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const repositoryUrl = getByText('http://my-repository/a/catalog-info.yaml'); + const kindText = getByText('Kind_2'); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).toBeVisible(); + expect(kindText).toBeInTheDocument(); + expect(kindText).not.toBeVisible(); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx new file mode 100644 index 0000000000..c10c011820 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx @@ -0,0 +1,56 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { CodeSnippet } from '@backstage/core'; +import { Card, CardContent, CardHeader } from '@material-ui/core'; +import React from 'react'; +import YAML from 'yaml'; + +type Props = { + repositoryUrl: string; + entities: Entity[]; + classes?: { card?: string; cardContent?: string }; +}; + +export const PreviewCatalogInfoComponent = ({ + repositoryUrl, + entities, + classes, +}: Props) => { + return ( + + {`${repositoryUrl.replace( + /[\/]*$/, + '', + )}/catalog-info.yaml`} + } + /> + + + YAML.stringify(e)) + .join('---\n') + .trim()} + language="yaml" + /> + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx new file mode 100644 index 0000000000..1556bd9b41 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { makeStyles } from '@material-ui/core'; +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; + +const useStyles = makeStyles({ + displayNone: { + display: 'none', + }, +}); + +describe('', () => { + it('renders without exploding', async () => { + const { getByText } = render( + , + ); + + const title = getByText('My Title'); + const description = getByText('description', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).toBeVisible(); + }); + + it('renders card with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const title = getByText('My Title'); + const description = getByText('description', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).not.toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).not.toBeVisible(); + }); + + it('renders with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const title = getByText('My Title'); + const description = getByText('description', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).not.toBeVisible(); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx new file mode 100644 index 0000000000..01af05d43a --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MarkdownContent } from '@backstage/core'; +import { Card, CardContent, CardHeader } from '@material-ui/core'; +import React from 'react'; + +type Props = { + title: string; + description: string; + classes?: { card?: string; cardContent?: string }; +}; + +export const PreviewPullRequestComponent = ({ + title, + description, + classes, +}: Props) => { + return ( + + + + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx new file mode 100644 index 0000000000..8980de4082 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -0,0 +1,288 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { TextField } from '@material-ui/core'; +import { act, render, RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest'; + +describe('', () => { + const catalogImportApi: jest.Mocked = { + analyzeUrl: jest.fn(), + submitPullRequest: jest.fn(), + }; + + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + addLocation: jest.fn(), + getEntityByName: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + const onPrepareFn = jest.fn(); + const analyzeResult = { + type: 'repository', + url: 'https://my-repository', + integrationType: 'github', + generatedEntities: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + spec: { + owner: 'my-owner', + }, + }, + ], + } as Extract; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders without exploding', async () => { + catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] })); + + await act(async () => { + const { getByText } = render( + { + return ( + <> + + + + + + ); + }} + />, + { + wrapper: Wrapper, + }, + ); + + const title = getByText('My title'); + const description = getByText('body', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).toBeVisible(); + }); + }); + + it('should submit created PR', async () => { + catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] })); + catalogImportApi.submitPullRequest.mockReturnValue( + Promise.resolve({ + location: 'https://my/location.yaml', + link: 'https://my/pull', + }), + ); + + let result: RenderResult; + await act(async () => { + result = await render( + { + return ( + <> + + + + + + ); + }} + />, + { + wrapper: Wrapper, + }, + ); + + await userEvent.type(await result.getByLabelText('name'), '-changed'); + await userEvent.type(await result.getByLabelText('owner'), '-changed'); + await userEvent.click( + await result.getByRole('button', { name: /Create PR/i }), + ); + }); + + expect(catalogImportApi.submitPullRequest).toBeCalledTimes(1); + expect(catalogImportApi.submitPullRequest.mock.calls[0]).toMatchObject([ + { + body: 'My **body**', + fileContent: `apiVersion: "1" +kind: Component +metadata: + name: my-component-changed + namespace: default +spec: + owner: my-owner-changed +`, + integrationType: 'github', + repositoryUrl: 'https://my-repository', + title: 'My title', + }, + ]); + expect(onPrepareFn).toBeCalledTimes(1); + expect(onPrepareFn.mock.calls[0]).toMatchObject([ + { + type: 'repository', + url: 'https://my-repository', + integrationType: 'github', + pullRequest: { + url: 'https://my/pull', + }, + locations: [ + { + entities: [ + { + kind: 'Component', + name: 'my-component-changed', + namespace: 'default', + }, + ], + target: 'https://my/location.yaml', + }, + ], + }, + { + notRepeatable: true, + }, + ]); + }); + + it('should show error message', async () => { + catalogApi.getEntities.mockResolvedValueOnce({ items: [] }); + catalogImportApi.submitPullRequest.mockRejectedValueOnce( + new Error('some error'), + ); + + let result: RenderResult; + await act(async () => { + result = await render( + { + return ( + <> + + + + + + ); + }} + />, + { + wrapper: Wrapper, + }, + ); + + await userEvent.click( + await result.getByRole('button', { name: /Create PR/i }), + ); + }); + + expect(result!.getByText('some error')).toBeInTheDocument(); + expect(catalogImportApi.submitPullRequest).toBeCalledTimes(1); + expect(onPrepareFn).toBeCalledTimes(0); + }); + + it('should load groups', async () => { + const renderFormFieldsFn = jest.fn(); + catalogApi.getEntities.mockReturnValue( + Promise.resolve({ + items: [ + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'my-group', + }, + }, + ], + }), + ); + + await act(async () => { + await render( + , + { + wrapper: Wrapper, + }, + ); + }); + + expect(catalogApi.getEntities).toBeCalledTimes(1); + expect(renderFormFieldsFn).toBeCalled(); + expect(renderFormFieldsFn.mock.calls[0][0]).toMatchObject({ + groups: [], + groupsLoading: true, + }); + expect( + renderFormFieldsFn.mock.calls[ + renderFormFieldsFn.mock.calls.length - 1 + ][0], + ).toMatchObject({ groups: ['Group:my-group'], groupsLoading: false }); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx new file mode 100644 index 0000000000..78a6fa158c --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -0,0 +1,247 @@ +/* + * 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 { Entity, serializeEntityRef } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Box, FormHelperText, Grid, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { useCallback, useState } from 'react'; +import { UseFormMethods } from 'react-hook-form'; +import { useAsync } from 'react-use'; +import YAML from 'yaml'; +import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { BackButton, NextButton } from '../Buttons'; +import { PrepareResult } from '../useImportState'; +import { PreparePullRequestForm } from './PreparePullRequestForm'; +import { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; +import { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; + +const useStyles = makeStyles(theme => ({ + previewCard: { + marginTop: theme.spacing(1), + }, + previewCardContent: { + paddingTop: 0, + }, +})); + +type FormData = { + title: string; + body: string; + componentName: string; + owner: string; +}; + +type Props = { + analyzeResult: Extract; + onPrepare: ( + result: PrepareResult, + opts?: { notRepeatable?: boolean }, + ) => void; + onGoBack?: () => void; + + defaultTitle: string; + defaultBody: string; + + renderFormFields: ( + props: Pick, 'errors' | 'register' | 'control'> & { + groups: string[]; + groupsLoading: boolean; + }, + ) => React.ReactNode; +}; + +function generateEntities( + entities: Entity[], + componentName: string, + owner: string, +): Entity[] { + return entities.map(e => ({ + ...e, + apiVersion: e.apiVersion!, + kind: e.kind!, + metadata: { + ...e.metadata, + name: componentName, + namespace: e.metadata?.namespace ?? 'default', + }, + spec: { + ...e.spec, + owner, + }, + })); +} + +export const StepPrepareCreatePullRequest = ({ + analyzeResult, + onPrepare, + onGoBack, + renderFormFields, + defaultTitle, + defaultBody, +}: Props) => { + const classes = useStyles(); + const catalogApi = useApi(catalogApiRef); + const catalogInfoApi = useApi(catalogImportApiRef); + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(); + + const { loading: groupsLoading, value: groups } = useAsync(async () => { + const groups = await catalogApi.getEntities({ + filter: { kind: 'group' }, + }); + + // TODO: defaultKind (=group), defaultNamespace (=same as entity) + return groups.items.map(e => serializeEntityRef(e) as string).sort(); + }); + + const handleResult = useCallback( + async (data: FormData) => { + setSubmitted(true); + + try { + const pr = await catalogInfoApi.submitPullRequest({ + repositoryUrl: analyzeResult.url, + integrationType: analyzeResult.integrationType, + title: data.title, + body: data.body, + fileContent: generateEntities( + analyzeResult.generatedEntities, + data.componentName, + data.owner, + ) + .map(e => YAML.stringify(e)) + .join('---\n'), + }); + + onPrepare( + { + type: 'repository', + url: analyzeResult.url, + integrationType: analyzeResult.integrationType, + pullRequest: { + url: pr.link, + }, + locations: [ + { + target: pr.location, + entities: generateEntities( + analyzeResult.generatedEntities, + data.componentName, + data.owner, + ).map(e => ({ + kind: e.kind, + namespace: e.metadata.namespace!, + name: e.metadata.name, + })), + }, + ], + }, + { notRepeatable: true }, + ); + } catch (e) { + setError(e.message); + setSubmitted(false); + } + }, + [ + analyzeResult.generatedEntities, + analyzeResult.integrationType, + analyzeResult.url, + catalogInfoApi, + onPrepare, + ], + ); + + return ( + <> + + You entered a link to a {analyzeResult.integrationType} repository but + we didn't found a catalog-info.yaml. Use this form to + create a Pull Request that creates an initial{' '} + catalog-info.yaml. + + + ( + <> + {renderFormFields({ + errors, + register, + control, + groups: groups ?? [], + groupsLoading, + })} + + + Preview Pull Request + + + + + + Preview Entities + + + + + {error && {error}} + + + {onGoBack && } + + Create PR + + + + )} + /> + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts new file mode 100644 index 0000000000..119feee4a1 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { AutocompleteTextField } from './AutocompleteTextField'; +export { PreparePullRequestForm } from './PreparePullRequestForm'; +export { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; +export { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; +export { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest'; diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx new file mode 100644 index 0000000000..82da9b9572 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx @@ -0,0 +1,202 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AnalyzeResult } from '../../api'; +import { StepPrepareSelectLocations } from './StepPrepareSelectLocations'; + +describe('', () => { + const analyzeResult = { + type: 'locations', + locations: [ + { + target: 'url', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + ], + }, + { + target: 'url-2', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + { + kind: 'api', + namespace: 'default', + name: 'name', + }, + ], + }, + ], + } as Extract; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders without exploding', async () => { + const { getByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + }); + + it('should select and deselect all', async () => { + const { getByRole, getAllByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + checkboxes.forEach(c => expect(c).not.toBeChecked()); + expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Select All/i })); + }); + + checkboxes.forEach(c => expect(c).toBeChecked()); + expect(getByRole('button', { name: /Review/i })).not.toBeDisabled(); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Select All/i })); + }); + + checkboxes.forEach(c => expect(c).not.toBeChecked()); + expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + }); + + it('should preselect prepared locations', async () => { + const { getAllByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + + expect(checkboxes[0]).not.toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[2]).not.toBeChecked(); + }); + + it('should select items', async () => { + const { getAllByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + checkboxes.forEach(c => expect(c).not.toBeChecked()); + + await act(async () => { + userEvent.click(checkboxes[1]); + }); + + expect(checkboxes[0]).not.toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[2]).not.toBeChecked(); + + await act(async () => { + userEvent.click(checkboxes[1]); + }); + + checkboxes.forEach(c => expect(c).not.toBeChecked()); + }); + + it('should go back', async () => { + const onGoBack = jest.fn(); + + const { getByRole } = render( + undefined} + onGoBack={onGoBack} + />, + ); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Back/i })); + }); + + expect(onGoBack).toBeCalledTimes(1); + }); + + it('should submit', async () => { + const onPrepare = jest.fn(); + + const { getAllByRole, getByRole } = render( + undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + + await act(async () => { + userEvent.click(checkboxes[1]); + }); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Review/i })); + }); + + expect(onPrepare).toBeCalledTimes(1); + expect(onPrepare.mock.calls[0][0]).toMatchObject({ + type: 'locations', + locations: [ + { + target: 'url', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + ], + }, + ], + } as Extract); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx new file mode 100644 index 0000000000..d44472fb5a --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx @@ -0,0 +1,124 @@ +/* + * 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 { + Checkbox, + Grid, + ListItem, + ListItemIcon, + ListItemText, + Typography, +} from '@material-ui/core'; +import React, { useCallback, useState } from 'react'; +import { AnalyzeResult } from '../../api'; +import { BackButton, NextButton } from '../Buttons'; +import { EntityListComponent } from '../EntityListComponent'; +import { PrepareResult } from '../useImportState'; + +type Props = { + analyzeResult: Extract; + prepareResult?: PrepareResult; + onPrepare: (result: PrepareResult) => void; + onGoBack?: () => void; +}; + +/** + * A form that lets a user select one of a list of locations to import + * + * @param analyzeResult the result of the analysis + * @param prepareResult the selectected locations from a previous step + * @param onPrepare called after the selection + * @param onGoBack called to go back to the previous step + */ +export const StepPrepareSelectLocations = ({ + analyzeResult, + prepareResult, + onPrepare, + onGoBack, +}: Props) => { + const [selectedUrls, setSelectedUrls] = useState( + prepareResult?.locations.map(l => l.target) || [], + ); + + const handleResult = useCallback(async () => { + onPrepare({ + type: 'locations', + locations: analyzeResult.locations.filter((l: any) => + selectedUrls.includes(l.target), + ), + }); + }, [analyzeResult.locations, onPrepare, selectedUrls]); + + const onItemClick = (url: string) => { + setSelectedUrls(urls => + urls.includes(url) ? urls.filter(u => u !== url) : urls.concat(url), + ); + }; + + const onSelectAll = () => { + setSelectedUrls(urls => + urls.length < analyzeResult.locations.length + ? analyzeResult.locations.map(l => l.target) + : [], + ); + }; + + return ( + <> + + Select one or more locations that are present in your git repository: + + + + + 0 && + selectedUrls.length < analyzeResult.locations.length + } + tabIndex={-1} + disableRipple + /> + + + + } + onItemClick={onItemClick} + locations={analyzeResult.locations} + locationListItemIcon={target => ( + + )} + collapsed + /> + + + {onGoBack && } + + Review + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts b/plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts new file mode 100644 index 0000000000..db98a63be0 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/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 { StepPrepareSelectLocations } from './StepPrepareSelectLocations'; diff --git a/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx new file mode 100644 index 0000000000..0f53f255ca --- /dev/null +++ b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx @@ -0,0 +1,134 @@ +/* + * 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 { configApiRef, Link, useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { FormHelperText, Grid, Typography } from '@material-ui/core'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import React, { useCallback, useState } from 'react'; +import { BackButton, NextButton } from '../Buttons'; +import { EntityListComponent } from '../EntityListComponent'; +import { PrepareResult, ReviewResult } from '../useImportState'; + +type Props = { + prepareResult: PrepareResult; + onReview: (result: ReviewResult) => void; + onGoBack?: () => void; +}; + +export const StepReviewLocation = ({ + prepareResult, + onReview, + onGoBack, +}: Props) => { + const catalogApi = useApi(catalogApiRef); + const configApi = useApi(configApiRef); + + const appTitle = configApi.getOptional('app.title') || 'Backstage'; + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(); + + const handleImport = useCallback(async () => { + setSubmitted(true); + try { + const result = await Promise.all( + prepareResult.locations.map(l => + catalogApi.addLocation({ + type: 'url', + target: l.target, + presence: + prepareResult.type === 'repository' ? 'optional' : 'required', + }), + ), + ); + + onReview({ + ...prepareResult, + locations: result.map(r => ({ + target: r.location.target, + entities: r.entities, + })), + }); + } catch (e) { + // TODO: this error should be handled differently. We add it as 'optional' and + // it is not uncommon that a PR has not been merged yet. + if ( + prepareResult.type === 'repository' && + e.message.startsWith( + 'Location was added but has no entities specified yet', + ) + ) { + onReview({ + ...prepareResult, + locations: prepareResult.locations.map(l => ({ + target: l.target, + entities: [], + })), + }); + } else { + setError(e.message); + setSubmitted(false); + } + } + }, [prepareResult, onReview, catalogApi]); + + return ( + <> + {prepareResult.type === 'repository' && ( + <> + + The following Pull Request has been opened:{' '} + + {prepareResult.pullRequest.url} + + + + + You can already import the location and {appTitle} will fetch the + entities as soon as the Pull Request is merged. + + + )} + + + The following entities will be added to the catalog: + + + } + /> + + {error && {error}} + + + {onGoBack && } + handleImport()} + > + Import + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepReviewLocation/index.ts b/plugins/catalog-import/src/components/StepReviewLocation/index.ts new file mode 100644 index 0000000000..2c93e98437 --- /dev/null +++ b/plugins/catalog-import/src/components/StepReviewLocation/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 { StepReviewLocation } from './StepReviewLocation'; diff --git a/plugins/catalog-import/src/components/index.ts b/plugins/catalog-import/src/components/index.ts new file mode 100644 index 0000000000..180752c873 --- /dev/null +++ b/plugins/catalog-import/src/components/index.ts @@ -0,0 +1,20 @@ +/* + * 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 './ImportStepper'; +export * from './EntityListComponent'; +export * from './StepInitAnalyzeUrl'; +export * from './StepPrepareCreatePullRequest'; diff --git a/plugins/catalog-import/src/components/useImportState.test.tsx b/plugins/catalog-import/src/components/useImportState.test.tsx new file mode 100644 index 0000000000..16bd5a8cd6 --- /dev/null +++ b/plugins/catalog-import/src/components/useImportState.test.tsx @@ -0,0 +1,358 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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, EntityName } from '@backstage/catalog-model'; +import { cleanup } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { AnalyzeResult } from '../api'; + +import { + ImportState, + PrepareResult, + ReviewResult, + useImportState, +} from './useImportState'; + +describe('useImportState', () => { + const as = ( + curr: ImportState, + _: T, + ) => curr as Extract; + + const locationAP: AnalyzeResult & PrepareResult = { + type: 'locations', + locations: [ + { + target: 'https://0', + entities: [] as EntityName[], + }, + ], + }; + + const locationR: ReviewResult = { + type: 'locations', + locations: [ + { + target: 'https://0', + entities: [] as Entity[], + }, + ], + }; + + it('should use initial url', async () => { + const { result } = renderHook(() => + useImportState({ initialUrl: 'http://my-url' }), + ); + await cleanup(); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: 'http://my-url', + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: undefined, + }); + }); + + describe('onAnalysis & onPrepare & onReview & onReset', () => { + it('should work', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: undefined, + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 1, + analysisUrl: 'http://my-url', + activeState: 'prepare', + analyzeResult: locationAP, + prepareResult: undefined, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'prepare').onPrepare(locationAP); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 2, + analysisUrl: 'http://my-url', + activeState: 'review', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 3, + analysisUrl: 'http://my-url', + activeState: 'finish', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: locationR, + }); + + act(() => result.current.onReset()); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: undefined, + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: locationR, + }); + }); + + it('should work skipped', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: undefined, + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + { + prepareResult: locationAP, + }, + ); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 2, + analysisUrl: 'http://my-url', + activeState: 'review', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 3, + analysisUrl: 'http://my-url', + activeState: 'finish', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: locationR, + }); + }); + + it('should throw on invalid state', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + // state 'analyze' + await expect( + act(async () => as(result.current, 'prepare').onPrepare(locationAP)), + ).rejects.toThrow(); + await expect( + act(async () => as(result.current, 'review').onReview(locationR)), + ).rejects.toThrow(); + + // switch state to 'prepare' + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ); + + await expect(async () => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ).rejects.toThrow(); + await expect( + act(async () => as(result.current, 'review').onReview(locationR)), + ).rejects.toThrow(); + + // switch to 'review' + act(() => as(result.current, 'prepare').onPrepare(locationAP)); + + await expect(async () => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ).rejects.toThrow(); + await expect( + act(async () => as(result.current, 'prepare').onPrepare(locationAP)), + ).rejects.toThrow(); + + // switch to 'finish' + act(() => as(result.current, 'review').onReview(locationR)); + }); + }); + + describe('onGoBack', () => { + it('should work', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.activeStepNumber).toBe(0); + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ); + expect(result.current.activeStepNumber).toBe(1); + + expect(result.current.onGoBack).not.toBeUndefined(); + act(() => result.current.onGoBack!()); + expect(result.current.activeStepNumber).toBe(0); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ); + act(() => as(result.current, 'prepare').onPrepare(locationAP)); + expect(result.current.activeStepNumber).toBe(2); + + expect(result.current.onGoBack).not.toBeUndefined(); + act(() => result.current.onGoBack!()); + expect(result.current.activeStepNumber).toBe(1); + + act(() => as(result.current, 'prepare').onPrepare(locationAP)); + act(() => as(result.current, 'review').onReview(locationR)); + expect(result.current.activeStepNumber).toBe(3); + }); + + it('should work for skipped', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.activeStepNumber).toBe(0); + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + { + prepareResult: locationAP, + }, + ), + ); + expect(result.current.activeStepNumber).toBe(2); + expect(result.current.onGoBack).not.toBeUndefined(); + + act(() => result.current.onGoBack!()); + expect(result.current.activeStepNumber).toBe(0); + expect(result.current.onGoBack).toBeUndefined(); + }); + + describe('should consider prepareNotRepeatable', () => { + it('as true', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ), + ); + act(() => + as(result.current, 'prepare').onPrepare(locationAP, { + notRepeatable: true, + }), + ); + + expect(result.current.onGoBack).toBeUndefined(); + }); + + it('as false', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ), + ); + act(() => + as(result.current, 'prepare').onPrepare(locationAP, { + notRepeatable: false, + }), + ); + + expect(result.current.onGoBack).not.toBeUndefined(); + }); + }); + }); +}); diff --git a/plugins/catalog-import/src/components/useImportState.ts b/plugins/catalog-import/src/components/useImportState.ts new file mode 100644 index 0000000000..b5fad52459 --- /dev/null +++ b/plugins/catalog-import/src/components/useImportState.ts @@ -0,0 +1,262 @@ +/* + * 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 { Entity, EntityName } from '@backstage/catalog-model'; +import { useCallback, useState } from 'react'; +import { AnalyzeResult } from '../api'; + +// the configuration of the stepper +export type ImportFlows = + | 'unknown' + | 'single-location' + | 'multiple-locations' + | 'no-location'; + +type ImportStateTypes = 'analyze' | 'prepare' | 'review' | 'finish'; + +// a list that shows the order of states +const importStates: ImportStateTypes[] = [ + 'analyze', + 'prepare', + 'review', + 'finish', +]; + +// result of the prepare state +export type PrepareResult = + | { + type: 'locations'; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + pullRequest: { + url: string; + }; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + }; + +// result of the review result +export type ReviewResult = + | { + type: 'locations'; + locations: Array<{ + target: string; + entities: Entity[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + pullRequest: { + url: string; + }; + locations: Array<{ + target: string; + entities: Entity[]; + }>; + }; + +// the type interfaces that are available in each state. every state provides +// already known information and means to go to the next, or the previous step. +type State = + | { + activeState: 'analyze'; + onAnalysis: ( + flow: ImportFlows, + url: string, + result: AnalyzeResult, + opts?: { prepareResult?: PrepareResult }, + ) => void; + } + | { + activeState: 'prepare'; + analyzeResult: AnalyzeResult; + prepareResult?: PrepareResult; + onPrepare: ( + result: PrepareResult, + opts?: { notRepeatable?: boolean }, + ) => void; + } + | { + activeState: 'review'; + analyzeResult: AnalyzeResult; + prepareResult: PrepareResult; + onReview: (result: ReviewResult) => void; + } + | { + activeState: 'finish'; + analyzeResult: AnalyzeResult; + prepareResult: PrepareResult; + reviewResult: ReviewResult; + }; + +export type ImportState = State & { + activeFlow: ImportFlows; + activeStepNumber: number; + analysisUrl?: string; + + onGoBack?: () => void; + onReset: () => void; +}; + +/** + * A hook that manages the state machine of the form. It handles different flows + * which each can implement up to four states: + * 1. analyze + * 2. preview (skippable from analyze->review) + * 3. review + * 4. finish + * + * @param opts options + */ +export const useImportState = (opts?: { initialUrl?: string }): ImportState => { + // state management for the results of the individual states + const [results, setResults] = useState<{ + activeFlow: ImportFlows; + activeState: ImportStateTypes; + analysisUrl?: string; + analyzeResult?: AnalyzeResult; + prepareResult?: PrepareResult; + reviewResult?: ReviewResult; + }>({ + activeFlow: 'unknown', + activeState: importStates[0], + analysisUrl: opts?.initialUrl, + }); + + // for going back + const [previewsSteps, setPreviousSteps] = useState([]); + + // compute the next state to use + const prepareNextStep = useCallback( + ( + fromState: ImportStateTypes, + opts?: { skip?: boolean; clearHistory?: boolean }, + ) => { + if (opts?.clearHistory) { + setPreviousSteps([]); + } else { + setPreviousSteps(s => s.concat(fromState)); + } + return importStates[ + importStates.indexOf(fromState) + (opts?.skip ? 2 : 1) + ]; + }, + [], + ); + + const onGoBack = useCallback(() => { + if (previewsSteps.length > 0) { + setResults(old => ({ + ...old, + activeState: previewsSteps[previewsSteps.length - 1], + })); + setPreviousSteps(previewsSteps.slice(0, previewsSteps.length - 1)); + } + }, [previewsSteps]); + + const onAnalysis = ( + flow: ImportFlows, + url: string, + result: AnalyzeResult, + opts?: { prepareResult?: PrepareResult }, + ) => { + if (results.activeState !== 'analyze') { + throw new Error("Can't analyze!"); + } + + setResults(old => ({ + ...old, + analysisUrl: url, + activeFlow: flow, + activeState: prepareNextStep('analyze', { + skip: opts?.prepareResult !== undefined, + }), + analyzeResult: result, + prepareResult: opts?.prepareResult, + })); + }; + + const onPrepare = ( + result: PrepareResult, + opts?: { notRepeatable?: boolean }, + ) => { + if (results.activeState !== 'prepare') { + throw new Error("Can't prepare!"); + } + + setResults(old => ({ + ...old, + prepareResult: result, + activeState: prepareNextStep('prepare', { + clearHistory: opts?.notRepeatable, + }), + })); + }; + + const onReview = (result: ReviewResult) => { + if (results.activeState !== 'review') { + throw new Error("Can't prepare!"); + } + + setResults(old => ({ + ...old, + reviewResult: result, + activeState: prepareNextStep('review'), + })); + }; + + const onReset = useCallback(() => { + setResults(old => ({ + activeFlow: 'unknown', + activeState: importStates[0], + analysisUrl: opts?.initialUrl, + + // we keep the old reviewResult since the form is animated and an + // undefined value might crash the last step. + reviewResult: old.reviewResult, + })); + setPreviousSteps([]); + }, [opts?.initialUrl]); + + return { + activeFlow: results.activeFlow, + activeStepNumber: importStates.indexOf(results.activeState), + analysisUrl: results.analysisUrl, + + activeState: results.activeState, + analyzeResult: results.analyzeResult!, + prepareResult: results.prepareResult!, + reviewResult: results.reviewResult!, + + onAnalysis, + onPrepare, + onReview, + + onGoBack: previewsSteps.length > 0 ? onGoBack : undefined, + onReset, + }; +}; diff --git a/plugins/catalog-import/src/index.ts b/plugins/catalog-import/src/index.ts index 7b4fc17121..562e0499f3 100644 --- a/plugins/catalog-import/src/index.ts +++ b/plugins/catalog-import/src/index.ts @@ -20,4 +20,5 @@ export { CatalogImportPage, } from './plugin'; export { Router } from './components/Router'; +export * from './components'; export * from './api'; diff --git a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json deleted file mode 100644 index eaadf1054a..0000000000 --- a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "existingEntityFiles": [], - "generateEntities": [ - { - "entity": { - "apiVersion": "backstage.io/v1alpha1", - "kind": "Component", - "metadata": { - "name": "somerepo", - "annotations": { - "github.com/project-slug": "someuser/somerepo" - } - }, - "spec": { - "type": "other", - "lifecycle": "unknown" - } - }, - "fields": [] - } - ] -} diff --git a/plugins/catalog-import/src/mocks/locations-POST-response.json b/plugins/catalog-import/src/mocks/locations-POST-response.json deleted file mode 100644 index c44a0f4d61..0000000000 --- a/plugins/catalog-import/src/mocks/locations-POST-response.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "location": { - "id": "d4a64359-a709-4c91-a9de-0905a033bf22", - "type": "url", - "target": "https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml" - }, - "entities": [ - { - "metadata": { - "namespace": "default", - "annotations": { - "backstage.io/managed-by-location": "url:https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml", - "github.com/project-slug": "someusername/somerepo" - }, - "name": "somerepo", - "uid": "e992d5ee-7c70-4316-90cf-325f1a0a5146", - "etag": "YWE2M2Q5MzgtNjdkNi00N2QwLWJkZjYtNDM0MTMzMDI4Y2I0", - "generation": 1 - }, - "apiVersion": "backstage.io/v1alpha1", - "kind": "Component", - "spec": { - "type": "other", - "lifecycle": "unknown", - "owner": "unknown" - }, - "relations": [ - { - "target": { - "kind": "group", - "namespace": "default", - "name": "unknown" - }, - "type": "ownedBy" - } - ] - } - ] -} diff --git a/plugins/catalog-import/src/plugin.ts b/plugins/catalog-import/src/plugin.ts index 42210d0ed1..ad759f8d9c 100644 --- a/plugins/catalog-import/src/plugin.ts +++ b/plugins/catalog-import/src/plugin.ts @@ -15,16 +15,16 @@ */ import { + configApiRef, createApiFactory, createPlugin, + createRoutableExtension, createRouteRef, discoveryApiRef, githubAuthApiRef, - configApiRef, - createRoutableExtension, } from '@backstage/core'; -import { catalogImportApiRef } from './api/CatalogImportApi'; -import { CatalogImportClient } from './api/CatalogImportClient'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogImportApiRef, CatalogImportClient } from './api'; export const rootRouteRef = createRouteRef({ path: '', @@ -40,9 +40,15 @@ export const catalogImportPlugin = createPlugin({ discoveryApi: discoveryApiRef, githubAuthApi: githubAuthApiRef, configApi: configApiRef, + catalogApi: catalogApiRef, }, - factory: ({ discoveryApi, githubAuthApi, configApi }) => - new CatalogImportClient({ discoveryApi, githubAuthApi, configApi }), + factory: ({ discoveryApi, githubAuthApi, configApi, catalogApi }) => + new CatalogImportClient({ + discoveryApi, + githubAuthApi, + configApi, + catalogApi, + }), }), ], routes: { diff --git a/plugins/catalog-import/src/setupTests.ts b/plugins/catalog-import/src/setupTests.ts index aea2220869..825bcd4115 100644 --- a/plugins/catalog-import/src/setupTests.ts +++ b/plugins/catalog-import/src/setupTests.ts @@ -15,4 +15,3 @@ */ import '@testing-library/jest-dom'; -import 'cross-fetch/polyfill'; diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts deleted file mode 100644 index a2d299b9f4..0000000000 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ /dev/null @@ -1,121 +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 YAML from 'yaml'; -import { useApi, configApiRef } from '@backstage/core'; -import { catalogImportApiRef } from '../api/CatalogImportApi'; -import { ConfigSpec } from '../components/ImportComponentPage'; -import parseGitUrl from 'git-url-parse'; - -// TODO: (O5ten) Refactor into a core API instead of direct usage like this -// https://github.com/backstage/backstage/pull/3613#issuecomment-7408929430 -import { - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, -} from '@backstage/integration'; - -export function useGithubRepos() { - const api = useApi(catalogImportApiRef); - const config = useApi(configApiRef); - - const getGithubIntegrationConfig = (location: string) => { - const { - name: repoName, - owner: ownerName, - resource: hostname, - } = parseGitUrl(location); - - const configs = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - const githubIntegrationConfig = configs.find(v => v.host === hostname); - if (!githubIntegrationConfig) { - throw new Error( - `Unable to locate github-integration for repo-location, ${location}`, - ); - } - return { - repoName, - ownerName, - githubIntegrationConfig, - }; - }; - - const submitPrToRepo = async (selectedRepo: ConfigSpec) => { - const { - repoName, - ownerName, - githubIntegrationConfig, - } = getGithubIntegrationConfig(selectedRepo.location); - const submitPRResponse = await api - .submitPrToRepo({ - owner: ownerName, - repo: repoName, - fileContent: selectedRepo.config - .map(entity => `---\n${YAML.stringify(entity)}`) - .join('\n'), - githubIntegrationConfig, - }) - .catch(e => { - throw new Error(`Failed to submit PR to repo, ${e.message}`); - }); - - await api - .createRepositoryLocation({ - location: submitPRResponse.location, - }) - .catch(e => { - throw new Error(`Failed to create repository location, ${e.message}`); - }); - - return submitPRResponse; - }; - - const checkForExistingCatalogInfo = async ( - location: string, - ): Promise<{ exists: boolean; url?: string }> => { - let githubConfig: { - repoName: string; - ownerName: string; - githubIntegrationConfig: GitHubIntegrationConfig; - }; - try { - githubConfig = getGithubIntegrationConfig(location); - } catch (e) { - return { exists: false }; - } - return await api - .checkForExistingCatalogInfo({ - owner: githubConfig.ownerName, - repo: githubConfig.repoName, - githubIntegrationConfig: githubConfig.githubIntegrationConfig, - }) - .catch(e => { - throw new Error( - `Failed to inspect repository for existing catalog-info.yaml, ${e.message}`, - ); - }); - }; - - return { - submitPrToRepo, - checkForExistingCatalogInfo, - generateEntityDefinitions: (repo: string) => - api.generateEntityDefinitions({ repo }), - addLocation: (location: string) => - api.createRepositoryLocation({ location }), - }; -} diff --git a/plugins/catalog-import/src/util/validate.test.ts b/plugins/catalog-import/src/util/validate.test.ts deleted file mode 100644 index 139c7fb0f8..0000000000 --- a/plugins/catalog-import/src/util/validate.test.ts +++ /dev/null @@ -1,33 +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 { ComponentIdValidators } from './validate'; - -describe('ComponentIdValidators', () => { - describe('httpsValidator validator', () => { - const errorMessage = 'Must start with https://.'; - test.each([ - [true, 'https://example.com'], - [errorMessage, 'http://example.com'], - [errorMessage, 'example.com'], - [errorMessage, 'www.example.com'], - [errorMessage, ''], - [errorMessage, undefined], - ])('should return %p for %s', (expected: string | boolean, arg: any) => { - expect(ComponentIdValidators.httpsValidator(arg)).toBe(expected); - }); - }); -}); From a346105b743e03c03239e7e2ad1e09d504eb8750 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Mon, 1 Feb 2021 16:51:33 +0100 Subject: [PATCH 034/187] Update the create-app template --- .changeset/late-books-greet.md | 1 + .../templates/default-app/packages/app/src/App.tsx | 9 +-------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.changeset/late-books-greet.md b/.changeset/late-books-greet.md index 9beff968e8..512aa4ecca 100644 --- a/.changeset/late-books-greet.md +++ b/.changeset/late-books-greet.md @@ -1,5 +1,6 @@ --- 'example-app': patch +'@backstage/create-app': patch '@backstage/plugin-catalog-import': minor --- diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 6deedcd748..ca2fc3564f 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -4,7 +4,6 @@ import { AlertDisplay, OAuthRequestDialog, SidebarPage, - createRouteRef, FlatRoutes, } from '@backstage/core'; import { apis } from './apis'; @@ -29,12 +28,6 @@ const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); const deprecatedAppRoutes = app.getRoutes(); -const catalogRouteRef = createRouteRef({ - path: '/catalog', - title: 'Service Catalog', -}); - - const App = () => ( @@ -55,7 +48,7 @@ const App = () => ( /> } + element={} /> Date: Thu, 4 Feb 2021 11:35:21 +0100 Subject: [PATCH 035/187] Add luxon and remove moment --- plugins/auth-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 27cb725faa..439b60e3ce 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -47,7 +47,7 @@ "jose": "^1.27.1", "jwt-decode": "^3.1.0", "knex": "^0.21.6", - "moment": "^2.26.0", + "luxon": "^1.25.0", "morgan": "^1.10.0", "node-cache": "^5.1.2", "openid-client": "^4.2.1", From 306dc12f3a11e99383d294b0fb905359e9c3821f Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 11:36:40 +0100 Subject: [PATCH 036/187] Port moment to luxon --- .../src/identity/DatabaseKeyStore.test.ts | 3 +-- .../auth-backend/src/identity/DatabaseKeyStore.ts | 9 ++++----- .../auth-backend/src/identity/MemoryKeyStore.ts | 6 +++--- plugins/auth-backend/src/identity/TokenFactory.ts | 14 +++++++------- plugins/auth-backend/src/identity/types.ts | 2 +- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts index b22ab0ecda..94dd442b44 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts @@ -15,7 +15,6 @@ */ import Knex from 'knex'; -import moment from 'moment'; import { DatabaseKeyStore } from './DatabaseKeyStore'; function createDB() { @@ -51,7 +50,7 @@ describe('DatabaseKeyStore', () => { const { items } = await store.listKeys(); expect(items).toEqual([{ createdAt: expect.anything(), key }]); - expect(Math.abs(items[0].createdAt.diff(moment(), 's'))).toBeLessThan(10); + expect(Math.abs(items[0].createdAt.diffNow('seconds').seconds)).toBeLessThan(10); }); it('should remove stored keys', async () => { diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index c24cac55be..cc4838cf9b 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -15,9 +15,9 @@ */ import Knex from 'knex'; -import { utc } from 'moment'; import { resolvePackagePath } from '@backstage/backend-common'; import { AnyJWK, KeyStore, StoredKey } from './types'; +import { DateTime } from 'luxon'; const migrationsDir = resolvePackagePath( '@backstage/plugin-auth-backend', @@ -62,12 +62,11 @@ export class DatabaseKeyStore implements KeyStore { async listKeys(): Promise<{ items: StoredKey[] }> { const rows = await this.database(TABLE).select(); - return { items: rows.map(row => ({ - key: JSON.parse(row.key), - createdAt: utc(row.created_at), - })), + key: JSON.parse(row.key), + createdAt: DateTime.fromFormat(row.created_at, 'yyyy-MM-dd HH:mm:ss', {zone: 'UTC'}), + })), }; } diff --git a/plugins/auth-backend/src/identity/MemoryKeyStore.ts b/plugins/auth-backend/src/identity/MemoryKeyStore.ts index b35002b146..0903f6987d 100644 --- a/plugins/auth-backend/src/identity/MemoryKeyStore.ts +++ b/plugins/auth-backend/src/identity/MemoryKeyStore.ts @@ -14,18 +14,18 @@ * limitations under the License. */ -import { utc } from 'moment'; import { KeyStore, AnyJWK, StoredKey } from './types'; +import { DateTime } from 'luxon'; export class MemoryKeyStore implements KeyStore { private readonly keys = new Map< string, - { createdAt: moment.Moment; key: string } + { createdAt: DateTime; key: string } >(); async addKey(key: AnyJWK): Promise { this.keys.set(key.kid, { - createdAt: utc(), + createdAt: DateTime.local().toUTC(), key: JSON.stringify(key), }); } diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 36622332e0..6874b08789 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import moment from 'moment'; import { TokenIssuer, TokenParams, KeyStore, AnyJWK } from './types'; import { JSONWebKey, JWK, JWS } from 'jose'; import { Logger } from 'winston'; import { v4 as uuid } from 'uuid'; +import { DateTime } from 'luxon'; const MS_IN_S = 1000; @@ -52,7 +52,7 @@ export class TokenFactory implements TokenIssuer { private readonly keyStore: KeyStore; private readonly keyDurationSeconds: number; - private keyExpiry?: moment.Moment; + private keyExpiry?: DateTime; private privateKeyPromise?: Promise; constructor(options: Options) { @@ -90,8 +90,8 @@ export class TokenFactory implements TokenIssuer { for (const key of keys) { // Allow for a grace period of another full key duration before we remove the keys from the database - const expireAt = key.createdAt.add(3 * this.keyDurationSeconds, 's'); - if (expireAt.isBefore()) { + const expireAt = key.createdAt.plus({ seconds: 3 * this.keyDurationSeconds }); + if (expireAt < DateTime.local()) { expiredKeys.push(key); } else { validKeys.push(key); @@ -115,16 +115,16 @@ export class TokenFactory implements TokenIssuer { } private async getKey(): Promise { - // Make sure that we only generate one key at a time + // Make sure that we only generate one key at a time¨ if (this.privateKeyPromise) { - if (this.keyExpiry?.isAfter()) { + if (this.keyExpiry > DateTime.local()) { return this.privateKeyPromise; } this.logger.info(`Signing key has expired, generating new key`); delete this.privateKeyPromise; } - this.keyExpiry = moment().add(this.keyDurationSeconds, 'seconds'); + this.keyExpiry = DateTime.local().plus({ seconds: this.keyDurationSeconds }); const promise = (async () => { // This generates a new signing key to be used to sign tokens until the next key rotation const key = await JWK.generate('EC', 'P-256', { diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index 827aba51ab..ac2e662214 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -52,7 +52,7 @@ export type TokenIssuer = { */ export type StoredKey = { key: AnyJWK; - createdAt: moment.Moment; + createdAt: luxon.DateTime; }; /** From 3600ac3b05a6580b3f539387e467a8c9714443fc Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 11:52:01 +0100 Subject: [PATCH 037/187] Add change set --- .changeset/strange-olives-unite.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strange-olives-unite.md diff --git a/.changeset/strange-olives-unite.md b/.changeset/strange-olives-unite.md new file mode 100644 index 0000000000..5fba505e46 --- /dev/null +++ b/.changeset/strange-olives-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Migrated the package from using moment to luxon. #4278 From ef4b39a754ba8180961916b06ce997c33c126568 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Thu, 4 Feb 2021 11:54:10 +0100 Subject: [PATCH 038/187] Split the step label and content to be able to refactor the stepper from vertical to horizontal in the future --- .../ImportStepper/ImportStepper.tsx | 44 ++- .../src/components/ImportStepper/defaults.tsx | 296 +++++++++--------- 2 files changed, 171 insertions(+), 169 deletions(-) diff --git a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx index 791cf17965..851bab9ef7 100644 --- a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx +++ b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx @@ -15,13 +15,14 @@ */ import { configApiRef, InfoCard, useApi } from '@backstage/core'; -import { Stepper } from '@material-ui/core'; +import { Step, StepContent, Stepper } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import React, { useMemo } from 'react'; import { ImportFlows, ImportState, useImportState } from '../useImportState'; import { defaultGenerateStepper, defaultStepper, + StepConfiguration, StepperProvider, StepperProviderOpts, } from './defaults'; @@ -55,6 +56,15 @@ export const ImportStepper = ({ [generateStepper, state.activeFlow], ); + const render = (step: StepConfiguration) => { + return ( + + {step.stepLabel} + {step.content} + + ); + }; + return ( - {states.analyze( - state as Extract, - { apis: { configApi }, opts }, + {render( + states.analyze( + state as Extract, + { apis: { configApi }, opts }, + ), )} - {states.prepare( - state as Extract, - { apis: { configApi }, opts }, + {render( + states.prepare( + state as Extract, + { apis: { configApi }, opts }, + ), )} - {states.review( - state as Extract, - { apis: { configApi }, opts }, + {render( + states.review( + state as Extract, + { apis: { configApi }, opts }, + ), )} - {states.finish( - state as Extract, - { apis: { configApi }, opts }, + {render( + states.finish( + state as Extract, + { apis: { configApi }, opts }, + ), )} diff --git a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx index 954aeb20dd..110cda9aa7 100644 --- a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx +++ b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx @@ -15,14 +15,7 @@ */ import { ConfigApi } from '@backstage/core'; -import { - Box, - Step, - StepContent, - StepLabel, - TextField, - Typography, -} from '@material-ui/core'; +import { Box, StepLabel, TextField, Typography } from '@material-ui/core'; import React from 'react'; import { BackButton } from '../Buttons'; import { StepFinishImportLocation } from '../StepFinishImportLocation'; @@ -46,23 +39,28 @@ type StepperApis = { configApi: ConfigApi; }; +export type StepConfiguration = { + stepLabel: React.ReactElement; + content: React.ReactElement; +}; + export type StepperProvider = { analyze: ( s: Extract, opts: { apis: StepperApis; opts?: StepperProviderOpts }, - ) => React.ReactElement; + ) => StepConfiguration; prepare: ( s: Extract, opts: { apis: StepperApis; opts?: StepperProviderOpts }, - ) => React.ReactElement; + ) => StepConfiguration; review: ( s: Extract, opts: { apis: StepperApis; opts?: StepperProviderOpts }, - ) => React.ReactElement; + ) => StepConfiguration; finish: ( s: Extract, opts: { apis: StepperApis; opts?: StepperProviderOpts }, - ) => React.ReactElement; + ) => StepConfiguration; }; function defaultPreparePullRequest(apis: StepperApis) { @@ -97,8 +95,8 @@ export function defaultGenerateStepper( case 'single-location': return { ...defaults, - prepare: () => ( - + prepare: () => ({ + stepLabel: ( @@ -108,8 +106,9 @@ export function defaultGenerateStepper( > Select Locations - - ), + ), + content: <>, + }), }; // let the user select one or more of the discovered locations in the prepare step @@ -121,8 +120,8 @@ export function defaultGenerateStepper( return defaults.prepare(state, opts); } - return ( - + return { + stepLabel: ( @@ -132,16 +131,16 @@ export function defaultGenerateStepper( > Select Locations - - - - - ); + ), + content: ( + + ), + }; }, }; @@ -158,96 +157,89 @@ export function defaultGenerateStepper( defaultPreparePullRequest )(opts.apis); - return ( - - Create Pull Request + return { + stepLabel: Create Pull Request, + content: ( + ( + <> + + Pull Request Details + - - ( - <> - - - Pull Request Details - - + - + - + + Entity Configuration + - - - Entity Configuration - - + - - - - - )} - /> - - - ); + + + )} + /> + ), + }; }, }; @@ -257,53 +249,45 @@ export function defaultGenerateStepper( } export const defaultStepper: StepperProvider = { - analyze: (state, { opts }) => ( - - Select URL - - - - - ), + analyze: (state, { opts }) => ({ + stepLabel: Select URL, + content: ( + + ), + }), - prepare: state => ( - + prepare: state => ({ + stepLabel: ( Optional}> Import Actions - - - - - ), + ), + content: , + }), - review: state => ( - - Review - - - - - ), + review: state => ({ + stepLabel: Review, + content: ( + + ), + }), - finish: state => ( - - Finish - - - - - ), + finish: state => ({ + stepLabel: Finish, + content: ( + + ), + }), }; From bd29ebb6059ec3326d4a948fd01369c7e1bd517c Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 15:02:37 +0100 Subject: [PATCH 039/187] Fix tsc, prettier and import issues --- .../src/identity/DatabaseKeyStore.test.ts | 4 +++- .../auth-backend/src/identity/DatabaseKeyStore.ts | 15 +++++++++++---- plugins/auth-backend/src/identity/TokenFactory.ts | 10 +++++++--- plugins/auth-backend/src/identity/types.ts | 3 ++- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts index 94dd442b44..ec886856fe 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts @@ -50,7 +50,9 @@ describe('DatabaseKeyStore', () => { const { items } = await store.listKeys(); expect(items).toEqual([{ createdAt: expect.anything(), key }]); - expect(Math.abs(items[0].createdAt.diffNow('seconds').seconds)).toBeLessThan(10); + expect( + Math.abs(items[0].createdAt.diffNow('seconds').seconds), + ).toBeLessThan(10); }); it('should remove stored keys', async () => { diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index cc4838cf9b..a41abffc7b 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -27,7 +27,7 @@ const migrationsDir = resolvePackagePath( const TABLE = 'signing_keys'; type Row = { - created_at: Date; + created_at: Date; //row.created_at is a string after being returned from the database kid: string; key: string; }; @@ -62,11 +62,18 @@ export class DatabaseKeyStore implements KeyStore { async listKeys(): Promise<{ items: StoredKey[] }> { const rows = await this.database(TABLE).select(); + return { items: rows.map(row => ({ - key: JSON.parse(row.key), - createdAt: DateTime.fromFormat(row.created_at, 'yyyy-MM-dd HH:mm:ss', {zone: 'UTC'}), - })), + key: JSON.parse(row.key), + createdAt: DateTime.fromFormat( + (row.created_at as unknown) as string, + 'yyyy-MM-dd HH:mm:ss', + { + zone: 'UTC', + }, + ), + })), }; } diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 6874b08789..963251f42b 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -90,7 +90,9 @@ export class TokenFactory implements TokenIssuer { for (const key of keys) { // Allow for a grace period of another full key duration before we remove the keys from the database - const expireAt = key.createdAt.plus({ seconds: 3 * this.keyDurationSeconds }); + const expireAt = key.createdAt.plus({ + seconds: 3 * this.keyDurationSeconds, + }); if (expireAt < DateTime.local()) { expiredKeys.push(key); } else { @@ -117,14 +119,16 @@ export class TokenFactory implements TokenIssuer { private async getKey(): Promise { // Make sure that we only generate one key at a time¨ if (this.privateKeyPromise) { - if (this.keyExpiry > DateTime.local()) { + if (this.keyExpiry && this.keyExpiry > DateTime.local()) { return this.privateKeyPromise; } this.logger.info(`Signing key has expired, generating new key`); delete this.privateKeyPromise; } - this.keyExpiry = DateTime.local().plus({ seconds: this.keyDurationSeconds }); + this.keyExpiry = DateTime.local().plus({ + seconds: this.keyDurationSeconds, + }); const promise = (async () => { // This generates a new signing key to be used to sign tokens until the next key rotation const key = await JWK.generate('EC', 'P-256', { diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index ac2e662214..4e3eeb872e 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { DateTime } from 'luxon'; /** Represents any form of serializable JWK */ export interface AnyJWK extends Record { @@ -52,7 +53,7 @@ export type TokenIssuer = { */ export type StoredKey = { key: AnyJWK; - createdAt: luxon.DateTime; + createdAt: DateTime; }; /** From fc2371edeb88fd8d64d070e3bf1a2791bb71c374 Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 15:03:32 +0100 Subject: [PATCH 040/187] Update .changeset/strange-olives-unite.md Co-authored-by: Himanshu Mishra --- .changeset/strange-olives-unite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/strange-olives-unite.md b/.changeset/strange-olives-unite.md index 5fba505e46..a6b89f3bc4 100644 --- a/.changeset/strange-olives-unite.md +++ b/.changeset/strange-olives-unite.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend': minor --- -Migrated the package from using moment to luxon. #4278 +Migrated the package from using moment to Luxon. #4278 From 190f2358fd2fb3c4f0d8e1f184ac74ca493d8042 Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 15:05:34 +0100 Subject: [PATCH 041/187] Update plugins/auth-backend/src/identity/TokenFactory.ts Co-authored-by: Himanshu Mishra --- plugins/auth-backend/src/identity/TokenFactory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 963251f42b..99ea26a7fb 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -117,7 +117,7 @@ export class TokenFactory implements TokenIssuer { } private async getKey(): Promise { - // Make sure that we only generate one key at a time¨ + // Make sure that we only generate one key at a time if (this.privateKeyPromise) { if (this.keyExpiry && this.keyExpiry > DateTime.local()) { return this.privateKeyPromise; From 2a49b2cc5fa9ebbcd21312230373cc3fc4cd5c1c Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 15:07:48 +0100 Subject: [PATCH 042/187] Change from minor to patch --- .changeset/strange-olives-unite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/strange-olives-unite.md b/.changeset/strange-olives-unite.md index a6b89f3bc4..7490f6096a 100644 --- a/.changeset/strange-olives-unite.md +++ b/.changeset/strange-olives-unite.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-auth-backend': minor +'@backstage/plugin-auth-backend': patch --- Migrated the package from using moment to Luxon. #4278 From a816fd2f33c524345154edb7f2acc521a363f3b1 Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 15:14:18 +0100 Subject: [PATCH 043/187] Update DatabaseKeyStore.ts --- plugins/auth-backend/src/identity/DatabaseKeyStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index a41abffc7b..b789522122 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -27,7 +27,7 @@ const migrationsDir = resolvePackagePath( const TABLE = 'signing_keys'; type Row = { - created_at: Date; //row.created_at is a string after being returned from the database + created_at: Date; // row.created_at is a string after being returned from the database kid: string; key: string; }; From 266f453f431a2ba9ad42006ed08e05892d67246a Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 30 Jan 2021 01:44:46 +0100 Subject: [PATCH 044/187] feat: added a test for running a simple cypress server --- .github/workflows/tugboat.yml | 42 +++++++- packages/e2e-test/cypress/plugins/index.js | 21 ++++ packages/e2e-test/package.json | 4 + packages/e2e-test/src/commands/cypress.ts | 34 +++++++ packages/e2e-test/src/commands/index.ts | 5 + .../e2e-test/src/cypress/support/index.d.ts | 28 ++++++ .../e2e-test/src/cypress/support/index.ts | 25 +++++ yarn.lock | 95 ++++++++++++++++++- 8 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 packages/e2e-test/cypress/plugins/index.js create mode 100644 packages/e2e-test/src/commands/cypress.ts create mode 100644 packages/e2e-test/src/cypress/support/index.d.ts create mode 100644 packages/e2e-test/src/cypress/support/index.ts diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index fdb2e1ddc4..6528e994c5 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -48,10 +48,42 @@ jobs: target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: '14' + - name: fetch branch master + run: git fetch origin master + + # Cache every node_modules folder inside the monorepo + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + # We use both yarn.lock and package.json as cache keys to ensure that + # changes to local monorepo packages bust the cache. + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + + # If we get a cache hit for node_modules, there's no need to bring in the global + # yarn cache or run yarn install, as all dependencies will be installed already. + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: yarn install + if: steps.cache-modules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile # This is required because the environment_url param that Tugboat uses # to tell us where the preview is located isn't supported unless you @@ -77,9 +109,12 @@ jobs: }); console.log(result); return result.data.environment_url; + - name: echo tugboat preview url - run: | - curl ${{steps.get-status-env.outputs.result}} + env: + BACKSTAGE_TEST_URL: ${{steps.get-status-env.outputs.result}} + run: yarn e2e-test cypress + - name: set status if: ${{ failure() }} uses: actions/github-script@v3 @@ -94,6 +129,7 @@ jobs: context: 'Backstage Tugboat E2E Tests', target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); + - name: set status if: ${{ success() }} uses: actions/github-script@v3 diff --git a/packages/e2e-test/cypress/plugins/index.js b/packages/e2e-test/cypress/plugins/index.js new file mode 100644 index 0000000000..aa9918d215 --- /dev/null +++ b/packages/e2e-test/cypress/plugins/index.js @@ -0,0 +1,21 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 4327d7161c..828788d7a4 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -41,5 +41,9 @@ "watch": "./src", "exec": "bin/e2e-test", "ext": "ts" + }, + "dependencies": { + "@types/cypress": "^1.1.3", + "cypress": "^6.4.0" } } diff --git a/packages/e2e-test/src/commands/cypress.ts b/packages/e2e-test/src/commands/cypress.ts new file mode 100644 index 0000000000..c1aa4d2b6b --- /dev/null +++ b/packages/e2e-test/src/commands/cypress.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 cypress from 'cypress'; +import path from 'path'; + +export async function run() { + await cypress.run({ + reporter: 'junit', + browser: 'chrome', + config: { + watchForFileChanges: true, + baseUrl: process.env.BACKSTAGE_TEST_URL || 'http://localhost:7000', + integrationFolder: path.resolve(__dirname, '../cypress/integration'), + supportFile: path.resolve(__dirname, '../cypress/support'), + fixturesFolder: false, + defaultCommandTimeout: 10000, + }, + configFile: false, + }); +} diff --git a/packages/e2e-test/src/commands/index.ts b/packages/e2e-test/src/commands/index.ts index 78ff3343f1..aad051e603 100644 --- a/packages/e2e-test/src/commands/index.ts +++ b/packages/e2e-test/src/commands/index.ts @@ -16,7 +16,12 @@ import { CommanderStatic } from 'commander'; import { run } from './run'; +import { run as cypress } from './cypress'; export function registerCommands(program: CommanderStatic) { program.command('run').description('Run e2e tests').action(run); + program + .command('cypress') + .description('Run cypress e2e tests') + .action(cypress); } diff --git a/packages/e2e-test/src/cypress/support/index.d.ts b/packages/e2e-test/src/cypress/support/index.d.ts new file mode 100644 index 0000000000..c9e765f23e --- /dev/null +++ b/packages/e2e-test/src/cypress/support/index.d.ts @@ -0,0 +1,28 @@ +/* + * 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. + */ +// in cypress/support/index.d.ts +// load type definitions that come with Cypress module +/// + +declare namespace Cypress { + interface Chainable { + /** + * Login as guest + * @example cy.loginAsGuests + */ + loginAsGuest(value: string): Chainable; + } +} diff --git a/packages/e2e-test/src/cypress/support/index.ts b/packages/e2e-test/src/cypress/support/index.ts new file mode 100644 index 0000000000..e17081831e --- /dev/null +++ b/packages/e2e-test/src/cypress/support/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ +/// + +Cypress.Commands.add('loginAsGuest', () => { + cy.visit('/', { + onLoad: (win: Window) => + win.localStorage.setItem('@backstage/core:SignInPage:provider', 'guest'), + }); +}); + +export {}; diff --git a/yarn.lock b/yarn.lock index 206d1f2705..6fdde6c0f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6618,6 +6618,13 @@ dependencies: postcss "5 - 7" +"@types/cypress@^1.1.3": + version "1.1.3" + resolved "https://registry.npmjs.org/@types/cypress/-/cypress-1.1.3.tgz#0a700c040d53e9e12b5af98e41d4a88c39f39b6a" + integrity sha512-OXe0Gw8LeCflkG1oPgFpyrYWJmEKqYncBsD/J0r17r0ETx/TnIGDNLwXt/pFYSYuYTpzcq1q3g62M9DrfsBL4g== + dependencies: + cypress "*" + "@types/d3-color@*": version "2.0.0" resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.0.tgz#febdfadade56e215a4c3f612fe3000d92999f5d5" @@ -9467,6 +9474,11 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +blob-util@2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + block-stream@*: version "0.0.9" resolved "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -10326,7 +10338,7 @@ cli-spinners@^2.2.0: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== -cli-table3@0.6.0: +cli-table3@0.6.0, cli-table3@~0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== @@ -11511,6 +11523,51 @@ cyclist@^1.0.1: resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= +cypress@*, cypress@^6.4.0: + version "6.4.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-6.4.0.tgz#432c516bf4f1a0f042a6aa1f2c3a4278fa35a8b2" + integrity sha512-SrsPsZ4IBterudkoFYBvkQmXOVxclh1/+ytbzpV8AH/D2FA+s2Qy5ISsaRzOFsbQa4KZWoi3AKwREmF1HucYkg== + dependencies: + "@cypress/listr-verbose-renderer" "^0.4.1" + "@cypress/request" "^2.88.5" + "@cypress/xvfb" "^1.2.4" + "@types/sinonjs__fake-timers" "^6.0.1" + "@types/sizzle" "^2.3.2" + arch "^2.1.2" + blob-util "2.0.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-table3 "~0.6.0" + commander "^5.1.0" + common-tags "^1.8.0" + dayjs "^1.9.3" + debug "^4.1.1" + eventemitter2 "^6.4.2" + execa "^4.0.2" + executable "^4.1.1" + extract-zip "^1.7.0" + fs-extra "^9.0.1" + getos "^3.2.1" + is-ci "^2.0.0" + is-installed-globally "^0.3.2" + lazy-ass "^1.6.0" + listr "^0.14.3" + lodash "^4.17.19" + log-symbols "^4.0.0" + minimist "^1.2.5" + moment "^2.29.1" + ospath "^1.2.2" + pretty-bytes "^5.4.1" + ramda "~0.26.1" + request-progress "^3.0.0" + supports-color "^7.2.0" + tmp "~0.2.1" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" + cypress@^4.2.0: version "4.12.1" resolved "https://registry.npmjs.org/cypress/-/cypress-4.12.1.tgz#0ead1b9f4c0917d69d8b57f996b6e01fe693b6ec" @@ -11788,6 +11845,11 @@ dateformat@^3.0.0: resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.9.3: + version "1.10.4" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" + integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== + dayjs@^1.9.4: version "1.9.4" resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.9.4.tgz#fcde984e227f4296f04e7b05720adad2e1071f1b" @@ -13286,6 +13348,21 @@ execa@^4.0.0, execa@^4.0.1: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +execa@^4.0.2: + version "4.1.0" + resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + executable@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" @@ -18954,7 +19031,7 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.19.3, moment@^2.25.3, moment@^2.26.0, moment@^2.27.0: +moment@^2.19.3, moment@^2.25.3, moment@^2.26.0, moment@^2.27.0, moment@^2.29.1: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -21329,6 +21406,11 @@ pretty-bytes@^5.3.0: resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== +pretty-bytes@^5.4.1: + version "5.5.0" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" + integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== + pretty-error@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" @@ -24626,6 +24708,13 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" @@ -25099,7 +25188,7 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" -tmp@^0.2.0: +tmp@^0.2.0, tmp@~0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== From df6ade0ba00a536c2db56955c43d0e933f05328c Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 16:35:15 +0100 Subject: [PATCH 045/187] Add luxon and remove moment --- plugins/catalog/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index c806c58e67..9e2d206467 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -42,7 +42,7 @@ "@types/react": "^16.9", "classnames": "^2.2.6", "git-url-parse": "^11.4.4", - "moment": "^2.26.0", + "luxon": "^1.25.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-helmet": "6.1.0", From e8532a062198a322822a6f975deba016c9ead75f Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 16:36:39 +0100 Subject: [PATCH 046/187] Port from moment to Luxon --- .../components/CatalogPage/utils/timeUtil.js | 10 ++++---- .../CatalogPage/utils/timeUtil.test.js | 24 +++++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js b/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js index 327f4177a8..4c5e98d97c 100644 --- a/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js +++ b/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import moment from 'moment'; +import { DateTime } from 'luxon'; import goodMorning from './locales/goodMorning.locales.json'; import goodAfternoon from './locales/goodAfternoon.locales.json'; import goodEvening from './locales/goodEvening.locales.json'; @@ -26,22 +26,22 @@ import goodEvening from './locales/goodEvening.locales.json'; * @returns {bool} Whether the date is valid or not. */ export function isValidDate(date) { - return moment(date).isValid(); + return DateTime.fromISO(date).isValid; } /** * Validates that a date is a valid ISO string and a specific format. * * @param date A date string - * @param format A format string or an array of format strings to validate against. + * @param format A format string to validate against. See https://moment.github.io/luxon/docs/manual/parsing.html#table-of-tokens for reference. * @returns {bool} Whether the date is valid or not according to the format. */ export function isValidDateAndFormat(date, format) { - return moment(date, format, true).isValid(); + return DateTime.fromFormat(date, format).isValid; } export function relativeTime(timestamp) { - return moment(timestamp).fromNow(); + return DateTime.fromISO(timestamp).toRelative(); } // Select a large random integer at startup, to prevent the greetings to change every time the user diff --git a/plugins/catalog/src/components/CatalogPage/utils/timeUtil.test.js b/plugins/catalog/src/components/CatalogPage/utils/timeUtil.test.js index d18b4a5b20..809d2aeac4 100644 --- a/plugins/catalog/src/components/CatalogPage/utils/timeUtil.test.js +++ b/plugins/catalog/src/components/CatalogPage/utils/timeUtil.test.js @@ -14,19 +14,29 @@ * limitations under the License. */ -import { isValidDateAndFormat, getTimeBasedGreeting } from './timeUtil'; +import { + isValidDate, + isValidDateAndFormat, + getTimeBasedGreeting, +} from './timeUtil'; +import { DateTime } from 'luxon'; + +it('validate isValidDate', () => { + expect(isValidDate(DateTime.local().toISO())).toBeTruthy(); + expect(isValidDate('2021-02-04 15:00:10')).toBeFalsy(); +}); it('validates time format', () => { const valid = isValidDateAndFormat( - '1970-01-01T00:00:00', - 'YYYY-MM-DD[T]HH:mm:ss', + '1970-01-01 00:00:00', + 'yyyy-MM-dd hh:mm:ss', ); const invalid = isValidDateAndFormat( - '1970/01/01T00:00:00', - 'YYYY-MM-DD[T]HH:mm:ss', + '1970-01-01T00:00:00', + 'yyyy-MM-ddThh:mm:ss', ); - expect(valid).toBe(true); - expect(invalid).toBe(false); + expect(valid).toBeTruthy(); + expect(invalid).toBeFalsy(); }); it('has greeting and language', () => { From e5da858d7a945a6fe160a3f2d17141e4f2237365 Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 16:38:17 +0100 Subject: [PATCH 047/187] Add changeset --- .changeset/pretty-fireants-taste.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pretty-fireants-taste.md diff --git a/.changeset/pretty-fireants-taste.md b/.changeset/pretty-fireants-taste.md new file mode 100644 index 0000000000..b3168a7671 --- /dev/null +++ b/.changeset/pretty-fireants-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Migrated the package from using moment to Luxon. #4278 From df5e7d4e43f2562fb72f463376025c89116f2abc Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 4 Feb 2021 17:40:35 +0200 Subject: [PATCH 048/187] Fixes following CR --- .../GithubDiscoveryProcessor.test.ts | 110 ++++++++++-------- .../processors/GithubDiscoveryProcessor.ts | 28 ++--- yarn.lock | 30 ++--- 3 files changed, 83 insertions(+), 85 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts index 9daecb26d3..fe6bb51521 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts @@ -18,27 +18,28 @@ import { getVoidLogger } from '@backstage/backend-common'; import { LocationSpec } from '@backstage/catalog-model'; import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; import { getOrganizationRepositories } from './github'; +import { ConfigReader } from '@backstage/config'; jest.mock('./github'); const mockGetOrganizationRepositories = getOrganizationRepositories as jest.MockedFunction< typeof getOrganizationRepositories >; -describe('GithubOrgReaderProcessor', () => { +describe('GithubDiscoveryProcessor', () => { describe('parseUrl', () => { it('parses well formed URLs', () => { expect( parseUrl('https://github.com/foo/proj/blob/master/catalog.yaml'), ).toEqual({ org: 'foo', - repoSearchPath: /proj/, + repoSearchPath: /^proj$/, catalogPath: 'blob/master/catalog.yaml', }); expect( parseUrl('https://github.com/foo/proj*/blob/master/catalog.yaml'), ).toEqual({ org: 'foo', - repoSearchPath: /proj.*/, + repoSearchPath: /^proj.*$/, catalogPath: 'blob/master/catalog.yaml', }); }); @@ -55,18 +56,14 @@ describe('GithubOrgReaderProcessor', () => { describe('reject unrelated entries', () => { it('rejects unknown types', async () => { - const processor = new GithubDiscoveryProcessor({ - gitHubConfigMap: new Map([ - [ - 'github.com', - { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }, - ], - ]), - logger: getVoidLogger(), - }); + const processor = GithubDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); const location: LocationSpec = { type: 'not-github-discovery', target: 'https://github.com', @@ -77,25 +74,17 @@ describe('GithubOrgReaderProcessor', () => { }); it('rejects unknown targets', async () => { - const processor = new GithubDiscoveryProcessor({ - gitHubConfigMap: new Map([ - [ - 'github.com', - { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }, - ], - [ - 'ghe.example.net', - { - host: 'ghe.example.net', - apiBaseUrl: 'https://ghe.example.net/api/v3', - }, - ], - ]), - logger: getVoidLogger(), - }); + const processor = GithubDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'blob' }, + { host: 'ghe.example.net', token: 'blob' }, + ], + }, + }), + { logger: getVoidLogger() }, + ); const location: LocationSpec = { type: 'github-discovery', target: 'https://not.github.com/apa', @@ -109,18 +98,14 @@ describe('GithubOrgReaderProcessor', () => { }); describe('handles repositories', () => { - const processor = new GithubDiscoveryProcessor({ - gitHubConfigMap: new Map([ - [ - 'github.com', - { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }, - ], - ]), - logger: getVoidLogger(), - }); + const processor = GithubDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); beforeEach(() => { mockGetOrganizationRepositories.mockClear(); @@ -160,7 +145,7 @@ describe('GithubOrgReaderProcessor', () => { }); }); - it('filter unrelated repositories', async () => { + it('output repositories with wildcards', async () => { const location: LocationSpec = { type: 'github-discovery', target: @@ -202,5 +187,36 @@ describe('GithubOrgReaderProcessor', () => { optional: false, }); }); + it('filter unrelated repositories', async () => { + const location: LocationSpec = { + type: 'github-discovery', + target: 'https://github.com/backstage/test/blob/master/catalog.yaml', + }; + mockGetOrganizationRepositories.mockResolvedValueOnce({ + repositories: [ + { name: 'abstest', url: 'https://github.com/backstage/abctest' }, + { + name: 'test', + url: 'https://github.com/backstage/test', + }, + { + name: 'testxyz', + url: 'https://github.com/backstage/testxyz', + }, + ], + }); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: 'https://github.com/backstage/test/blob/master/catalog.yaml', + }, + optional: false, + }); + }); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts index 698f0ad987..f96943eebb 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -18,8 +18,7 @@ import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { GithubCredentialsProvider, - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, + ScmIntegrations, } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { Logger } from 'winston'; @@ -31,26 +30,20 @@ import { CatalogProcessor, CatalogProcessorEmit } from './types'; * Extracts repositories out of a GitHub org. */ export class GithubDiscoveryProcessor implements CatalogProcessor { - private readonly gitHubConfigMap: Map; + private readonly integrations: ScmIntegrations; private readonly logger: Logger; static fromConfig(config: Config, options: { logger: Logger }) { - const configs = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - const gitHubConfigMap = new Map(configs.map(c => [c.host, c])); + const integrations = ScmIntegrations.fromConfig(config); return new GithubDiscoveryProcessor({ ...options, - gitHubConfigMap, + integrations, }); } - constructor(options: { - gitHubConfigMap: Map; - logger: Logger; - }) { - this.gitHubConfigMap = options.gitHubConfigMap; + constructor(options: { integrations: ScmIntegrations; logger: Logger }) { + this.integrations = options.integrations; this.logger = options.logger; } @@ -63,9 +56,8 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { return false; } - const gitHubConfig = this.gitHubConfigMap.get( - new URL(location.target).hostname, - ); + const gitHubConfig = this.integrations.github.byUrl(location.target) + ?.config; if (!gitHubConfig) { throw new Error( `There is no GitHub integration that matches ${location.target}. Please add a configuration entry for it under integrations.github`, @@ -83,7 +75,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { // Read out all of the raw data const startTimestamp = Date.now(); - this.logger.info('Reading GitHub repositories'); + this.logger.info(`Reading GitHub repositories from ${location.target}`); const { repositories } = await getOrganizationRepositories(client, org); @@ -134,5 +126,5 @@ export function parseUrl( } export function escapeRegExp(str: string): RegExp { - return new RegExp(str.replace(/\*/g, '.*')); + return new RegExp(`^${str.replace(/\*/g, '.*')}$`); } diff --git a/yarn.lock b/yarn.lock index 810090a45f..c20a8c505d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2473,11 +2473,9 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" - integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== + version "0.7.0" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2486,11 +2484,9 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" - integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== + version "0.7.0" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2499,17 +2495,16 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.3.2" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" - integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== + version "0.5.0" dependencies: - "@backstage/config" "^0.1.1" - "@backstage/core-api" "^0.2.1" - "@backstage/theme" "^0.2.1" + "@backstage/config" "^0.1.2" + "@backstage/core-api" "^0.2.8" + "@backstage/theme" "^0.2.2" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" @@ -2518,7 +2513,7 @@ d3-shape "^2.0.0" d3-zoom "^2.0.0" dagre "^0.8.5" - immer "^7.0.9" + immer "^8.0.1" lodash "^4.17.15" material-table "^1.69.1" prop-types "^15.7.2" @@ -15333,11 +15328,6 @@ immer@1.10.0: resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== -immer@^7.0.9: - version "7.0.15" - resolved "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz#dc3bc6db87401659d2e737c67a21b227c484a4ad" - integrity sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA== - immer@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" From d742b1fcd5722c0fad4abf8b72d9fb36cdb6d654 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 4 Feb 2021 17:44:57 +0200 Subject: [PATCH 049/187] Rebased and reinstalled yarn.lock --- yarn.lock | 83 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index 713ef89c0a..90e6f63ccd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2583,7 +2583,7 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.7.0" + version "0.7.1" dependencies: "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" @@ -2595,7 +2595,7 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.7.0" + version "0.7.1" dependencies: "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" @@ -2607,11 +2607,11 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.5.0" + version "0.6.0" dependencies: "@backstage/config" "^0.1.2" "@backstage/core-api" "^0.2.8" - "@backstage/theme" "^0.2.2" + "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" @@ -2645,17 +2645,69 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/integration@^0.3.1": - version "0.3.1" - resolved "https://registry.npmjs.org/@backstage/integration/-/integration-0.3.1.tgz#4bdf1883b7149f7d4866bf1732d8e371f37ed143" - integrity sha512-aHfZ83z3XmO3R1SPG3r3W9KjSab/aFJ+51LjvRr6OCthfFTIusEmFq4cro5RUn7IPTn/0L8qpBSW2xH6FlRuVw== +"@backstage/core@^0.5.0": + version "0.6.0" dependencies: "@backstage/config" "^0.1.2" - "@octokit/auth-app" "^2.10.5" - "@octokit/rest" "^18.0.12" - cross-fetch "^3.0.6" + "@backstage/core-api" "^0.2.8" + "@backstage/theme" "^0.2.3" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@testing-library/react-hooks" "^3.4.2" + "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" + "@types/react" "^16.9" + "@types/react-sparklines" "^1.7.0" + classnames "^2.2.6" + clsx "^1.1.0" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-zoom "^2.0.0" + dagre "^0.8.5" + immer "^8.0.1" + lodash "^4.17.15" + material-table "^1.69.1" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "^3.0.0" + react "^16.12.0" + react-dom "^16.12.0" + react-helmet "6.1.0" + react-hook-form "^6.6.0" + react-markdown "^5.0.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^13.5.1" + react-use "^15.3.3" + remark-gfm "^1.0.0" + zen-observable "^0.8.15" + +"@backstage/plugin-catalog@^0.2.0", "@backstage/plugin-catalog@^0.2.1": + version "0.2.14" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog/-/plugin-catalog-0.2.14.tgz#50a4176a55ffa543a426ec78cbc9deaecdbcf2b7" + integrity sha512-lDmNcC+m1zbbzYATUp5yIZ5PUp+YyBc1KKu3CCgqjLWSbJ1aJrU1N4g59euel1l2+qSW+lH76Kkp6ZYpZbSO9A== + dependencies: + "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-model" "^0.7.0" + "@backstage/core" "^0.5.0" + "@backstage/plugin-scaffolder" "^0.4.1" + "@backstage/theme" "^0.2.2" + "@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" git-url-parse "^11.4.4" - luxon "^1.25.0" + moment "^2.26.0" + react "^16.13.1" + react-dom "^16.13.1" + react-helmet "6.1.0" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + swr "^0.3.0" "@bcoe/v8-coverage@^0.2.3": version "0.2.3" @@ -14360,13 +14412,6 @@ git-url-parse@^11.4.4: dependencies: git-up "^4.0.0" -git-url-parse@^11.4.4: - version "11.4.4" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" - integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw== - dependencies: - git-up "^4.0.0" - gitconfiglocal@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" From b3b90acee7a4bf6df07a6880b6a89edb856fa631 Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 16:49:06 +0100 Subject: [PATCH 050/187] Changed to fromSQL --- plugins/auth-backend/src/identity/DatabaseKeyStore.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index b789522122..53c8bde307 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -66,9 +66,8 @@ export class DatabaseKeyStore implements KeyStore { return { items: rows.map(row => ({ key: JSON.parse(row.key), - createdAt: DateTime.fromFormat( + createdAt: DateTime.fromSQL( (row.created_at as unknown) as string, - 'yyyy-MM-dd HH:mm:ss', { zone: 'UTC', }, From f6e56e6cfb09c08f2bccf1765d33f2afa1f74a73 Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Thu, 4 Feb 2021 17:55:00 +0100 Subject: [PATCH 051/187] Fix prettier formatting --- plugins/auth-backend/src/identity/DatabaseKeyStore.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index 53c8bde307..6aad9af30f 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -66,12 +66,9 @@ export class DatabaseKeyStore implements KeyStore { return { items: rows.map(row => ({ key: JSON.parse(row.key), - createdAt: DateTime.fromSQL( - (row.created_at as unknown) as string, - { - zone: 'UTC', - }, - ), + createdAt: DateTime.fromSQL((row.created_at as unknown) as string, { + zone: 'UTC', + }), })), }; } From dccf6f64685caa4e7991d2286028a7553d154640 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 18:32:03 +0100 Subject: [PATCH 052/187] chore: reworking the lock file for the cypress deps --- packages/app/package.json | 2 +- yarn.lock | 65 --------------------------------------- 2 files changed, 1 insertion(+), 66 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 9de8299d93..949146833e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -65,7 +65,7 @@ "@types/react-dom": "^16.9.8", "@types/zen-observable": "^0.8.0", "cross-env": "^7.0.0", - "cypress": "^4.2.0", + "cypress": "^6.4.0", "eslint-plugin-cypress": "^2.10.3", "start-server-and-test": "^1.10.11" }, diff --git a/yarn.lock b/yarn.lock index 6fdde6c0f9..1c328d8a93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10348,16 +10348,6 @@ cli-table3@0.6.0, cli-table3@~0.6.0: optionalDependencies: colors "^1.1.2" -cli-table3@~0.5.1: - version "0.5.1" - resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" - integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== - dependencies: - object-assign "^4.1.0" - string-width "^2.1.1" - optionalDependencies: - colors "^1.1.2" - cli-truncate@2.1.0, cli-truncate@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" @@ -11568,49 +11558,6 @@ cypress@*, cypress@^6.4.0: url "^0.11.0" yauzl "^2.10.0" -cypress@^4.2.0: - version "4.12.1" - resolved "https://registry.npmjs.org/cypress/-/cypress-4.12.1.tgz#0ead1b9f4c0917d69d8b57f996b6e01fe693b6ec" - integrity sha512-9SGIPEmqU8vuRA6xst2CMTYd9sCFCxKSzrHt0wr+w2iAQMCIIsXsQ5Gplns1sT6LDbZcmLv6uehabAOl3fhc9Q== - dependencies: - "@cypress/listr-verbose-renderer" "^0.4.1" - "@cypress/request" "^2.88.5" - "@cypress/xvfb" "^1.2.4" - "@types/sinonjs__fake-timers" "^6.0.1" - "@types/sizzle" "^2.3.2" - arch "^2.1.2" - bluebird "^3.7.2" - cachedir "^2.3.0" - chalk "^2.4.2" - check-more-types "^2.24.0" - cli-table3 "~0.5.1" - commander "^4.1.1" - common-tags "^1.8.0" - debug "^4.1.1" - eventemitter2 "^6.4.2" - execa "^1.0.0" - executable "^4.1.1" - extract-zip "^1.7.0" - fs-extra "^8.1.0" - getos "^3.2.1" - is-ci "^2.0.0" - is-installed-globally "^0.3.2" - lazy-ass "^1.6.0" - listr "^0.14.3" - lodash "^4.17.19" - log-symbols "^3.0.0" - minimist "^1.2.5" - moment "^2.27.0" - ospath "^1.2.2" - pretty-bytes "^5.3.0" - ramda "~0.26.1" - request-progress "^3.0.0" - supports-color "^7.1.0" - tmp "~0.1.0" - untildify "^4.0.0" - url "^0.11.0" - yauzl "^2.10.0" - d3-array@^1.2.0: version "1.2.4" resolved "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" @@ -21401,11 +21348,6 @@ prettier@^2.0.5, prettier@~2.0.5: resolved "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== -pretty-bytes@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" - integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== - pretty-bytes@^5.4.1: version "5.5.0" resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" @@ -25195,13 +25137,6 @@ tmp@^0.2.0, tmp@~0.2.1: dependencies: rimraf "^3.0.0" -tmp@~0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" - integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== - dependencies: - rimraf "^2.6.3" - tmpl@1.0.x: version "1.0.4" resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" From 1164e40428627a43d0e46019961d6b3702896c79 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 19:45:08 +0100 Subject: [PATCH 053/187] chore: adding cypress.config --- packages/e2e-test/src/commands/cypress.ts | 8 ++------ packages/e2e-test/src/cypress.json | 7 +++++++ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 packages/e2e-test/src/cypress.json diff --git a/packages/e2e-test/src/commands/cypress.ts b/packages/e2e-test/src/commands/cypress.ts index c1aa4d2b6b..8d14c67402 100644 --- a/packages/e2e-test/src/commands/cypress.ts +++ b/packages/e2e-test/src/commands/cypress.ts @@ -16,6 +16,7 @@ import cypress from 'cypress'; import path from 'path'; +import config from '../cypress.json'; export async function run() { await cypress.run({ @@ -23,12 +24,7 @@ export async function run() { browser: 'chrome', config: { watchForFileChanges: true, - baseUrl: process.env.BACKSTAGE_TEST_URL || 'http://localhost:7000', - integrationFolder: path.resolve(__dirname, '../cypress/integration'), - supportFile: path.resolve(__dirname, '../cypress/support'), - fixturesFolder: false, - defaultCommandTimeout: 10000, + ...config, }, - configFile: false, }); } diff --git a/packages/e2e-test/src/cypress.json b/packages/e2e-test/src/cypress.json new file mode 100644 index 0000000000..7dba277a86 --- /dev/null +++ b/packages/e2e-test/src/cypress.json @@ -0,0 +1,7 @@ +{ + "baseUrl": "http://localhost:7000", + "integrationFolder": "./cypress/integration", + "supportFile": "./cypress/support", + "fixturesFolder": false, + "defaultCommandTimeout": 10000 +} From ebccd5aa90dc86ca079be5f4ce3b1b3eac08eb2c Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 20:06:37 +0100 Subject: [PATCH 054/187] chore: reworking some more examples --- packages/e2e-test/cypress/plugins/index.js | 21 ---------------- packages/e2e-test/src/commands/cypress.ts | 10 ++++---- packages/e2e-test/src/commands/index.ts | 1 + packages/e2e-test/src/cypress.json | 1 - .../index.d.ts => integration/catalog.ts} | 24 +++++++++---------- packages/e2e-test/src/types.d.ts | 10 ++++++++ 6 files changed, 28 insertions(+), 39 deletions(-) delete mode 100644 packages/e2e-test/cypress/plugins/index.js rename packages/e2e-test/src/cypress/{support/index.d.ts => integration/catalog.ts} (66%) diff --git a/packages/e2e-test/cypress/plugins/index.js b/packages/e2e-test/cypress/plugins/index.js deleted file mode 100644 index aa9918d215..0000000000 --- a/packages/e2e-test/cypress/plugins/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/// -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -/** - * @type {Cypress.PluginConfig} - */ -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -} diff --git a/packages/e2e-test/src/commands/cypress.ts b/packages/e2e-test/src/commands/cypress.ts index 8d14c67402..866362c3dc 100644 --- a/packages/e2e-test/src/commands/cypress.ts +++ b/packages/e2e-test/src/commands/cypress.ts @@ -15,16 +15,16 @@ */ import cypress from 'cypress'; -import path from 'path'; import config from '../cypress.json'; -export async function run() { - await cypress.run({ +export async function run({ watch }: { watch: boolean }) { + const command = watch ? cypress.open : cypress.run; + await command({ reporter: 'junit', browser: 'chrome', config: { - watchForFileChanges: true, - ...config, + watchForFileChanges: watch, }, + configFile: `${__dirname}/../cypress.json`, }); } diff --git a/packages/e2e-test/src/commands/index.ts b/packages/e2e-test/src/commands/index.ts index aad051e603..0d14f37135 100644 --- a/packages/e2e-test/src/commands/index.ts +++ b/packages/e2e-test/src/commands/index.ts @@ -23,5 +23,6 @@ export function registerCommands(program: CommanderStatic) { program .command('cypress') .description('Run cypress e2e tests') + .option('-w, --watch', 'watch/open mode') .action(cypress); } diff --git a/packages/e2e-test/src/cypress.json b/packages/e2e-test/src/cypress.json index 7dba277a86..78392d60da 100644 --- a/packages/e2e-test/src/cypress.json +++ b/packages/e2e-test/src/cypress.json @@ -2,6 +2,5 @@ "baseUrl": "http://localhost:7000", "integrationFolder": "./cypress/integration", "supportFile": "./cypress/support", - "fixturesFolder": false, "defaultCommandTimeout": 10000 } diff --git a/packages/e2e-test/src/cypress/support/index.d.ts b/packages/e2e-test/src/cypress/integration/catalog.ts similarity index 66% rename from packages/e2e-test/src/cypress/support/index.d.ts rename to packages/e2e-test/src/cypress/integration/catalog.ts index c9e765f23e..1e805ee06c 100644 --- a/packages/e2e-test/src/cypress/support/index.d.ts +++ b/packages/e2e-test/src/cypress/integration/catalog.ts @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// in cypress/support/index.d.ts -// load type definitions that come with Cypress module -/// +import 'os'; -declare namespace Cypress { - interface Chainable { - /** - * Login as guest - * @example cy.loginAsGuests - */ - loginAsGuest(value: string): Chainable; - } -} +describe('Catalog', () => { + describe('default entities', () => { + it('displays the correct amount of entities from default config', () => { + cy.loginAsGuest(); + + cy.visit('/catalog'); + + cy.get('table').should('be.visible'); + }); + }); +}); diff --git a/packages/e2e-test/src/types.d.ts b/packages/e2e-test/src/types.d.ts index 0ae86490ee..0f9fe9a475 100644 --- a/packages/e2e-test/src/types.d.ts +++ b/packages/e2e-test/src/types.d.ts @@ -13,6 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/// declare module 'zombie'; declare module 'pgtools'; +declare namespace Cypress { + interface Chainable { + /** + * Login as guest + * @example cy.loginAsGuests + */ + loginAsGuest(): Chainable; + } +} From ccaa7cb7df53fa3f47b0020317e927ae708c7297 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 20:14:46 +0100 Subject: [PATCH 055/187] chore: added some more simple stuff for e2e tests with cypress --- packages/e2e-test/src/commands/cypress.ts | 9 +++++++-- packages/e2e-test/src/cypress/fixtures/.gitkeep | 0 packages/e2e-test/src/cypress/plugins/index.ts | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 packages/e2e-test/src/cypress/fixtures/.gitkeep create mode 100644 packages/e2e-test/src/cypress/plugins/index.ts diff --git a/packages/e2e-test/src/commands/cypress.ts b/packages/e2e-test/src/commands/cypress.ts index 866362c3dc..be947c87c8 100644 --- a/packages/e2e-test/src/commands/cypress.ts +++ b/packages/e2e-test/src/commands/cypress.ts @@ -15,7 +15,7 @@ */ import cypress from 'cypress'; -import config from '../cypress.json'; +import path from 'path'; export async function run({ watch }: { watch: boolean }) { const command = watch ? cypress.open : cypress.run; @@ -24,7 +24,12 @@ export async function run({ watch }: { watch: boolean }) { browser: 'chrome', config: { watchForFileChanges: watch, + baseUrl: process.env.BACKSTAGE_TEST_URL ?? 'http://localhost:7000', + integrationFolder: path.resolve(__dirname, '../cypress/integration'), + supportFile: path.resolve(__dirname, '../cypress/support'), + fixturesFolder: path.resolve(__dirname, '../cypress/fixtures'), + pluginsFile: path.resolve(__dirname, '../cypress/plugins'), + defaultCommandTimeout: 10000, }, - configFile: `${__dirname}/../cypress.json`, }); } diff --git a/packages/e2e-test/src/cypress/fixtures/.gitkeep b/packages/e2e-test/src/cypress/fixtures/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/e2e-test/src/cypress/plugins/index.ts b/packages/e2e-test/src/cypress/plugins/index.ts new file mode 100644 index 0000000000..4e230aca20 --- /dev/null +++ b/packages/e2e-test/src/cypress/plugins/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 {}; From 93bfa73142c6bb2c79422feb937b8943e2792528 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 20:18:55 +0100 Subject: [PATCH 056/187] chore: reworking again --- packages/e2e-test/src/commands/cypress.ts | 1 + packages/e2e-test/src/cypress/plugins/index.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/e2e-test/src/commands/cypress.ts b/packages/e2e-test/src/commands/cypress.ts index be947c87c8..f0ee99da30 100644 --- a/packages/e2e-test/src/commands/cypress.ts +++ b/packages/e2e-test/src/commands/cypress.ts @@ -31,5 +31,6 @@ export async function run({ watch }: { watch: boolean }) { pluginsFile: path.resolve(__dirname, '../cypress/plugins'), defaultCommandTimeout: 10000, }, + configFile: false, }); } diff --git a/packages/e2e-test/src/cypress/plugins/index.ts b/packages/e2e-test/src/cypress/plugins/index.ts index 4e230aca20..b90c276d71 100644 --- a/packages/e2e-test/src/cypress/plugins/index.ts +++ b/packages/e2e-test/src/cypress/plugins/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export {}; +export default () => {}; From 20e032f7c1c974db25c6a8ae5c708e62e262af51 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 20:40:52 +0100 Subject: [PATCH 057/187] chore: making cypress nice --- packages/e2e-test/src/cypress.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/e2e-test/src/cypress.json b/packages/e2e-test/src/cypress.json index 78392d60da..54b24ebbf0 100644 --- a/packages/e2e-test/src/cypress.json +++ b/packages/e2e-test/src/cypress.json @@ -2,5 +2,7 @@ "baseUrl": "http://localhost:7000", "integrationFolder": "./cypress/integration", "supportFile": "./cypress/support", + "fixturesFolder": "./cypress/fixures", + "pluginFile": "./cypress/plugins", "defaultCommandTimeout": 10000 } From a4963ec785c313ee245c7a8b98741fc1ba0fcd98 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 20:45:35 +0100 Subject: [PATCH 058/187] chore: added a sample test that works --- packages/e2e-test/src/cypress/integration/catalog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-test/src/cypress/integration/catalog.ts b/packages/e2e-test/src/cypress/integration/catalog.ts index 1e805ee06c..3518bac68d 100644 --- a/packages/e2e-test/src/cypress/integration/catalog.ts +++ b/packages/e2e-test/src/cypress/integration/catalog.ts @@ -22,7 +22,7 @@ describe('Catalog', () => { cy.visit('/catalog'); - cy.get('table').should('be.visible'); + cy.contains('Owned (7)').should('be.visible'); }); }); }); From 83ba509d22f8f7677e4607b726077b2195daad45 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 20:57:01 +0100 Subject: [PATCH 059/187] chore: reworking tests to use he cypress github-action instead --- .github/workflows/tugboat.yml | 12 ++++++++---- packages/e2e-test/src/cypress/integration/catalog.ts | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 6528e994c5..8478701957 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -110,11 +110,15 @@ jobs: console.log(result); return result.data.environment_url; - - name: echo tugboat preview url + - name: Cypress run + uses: cypress-io/github-action@v2 env: - BACKSTAGE_TEST_URL: ${{steps.get-status-env.outputs.result}} - run: yarn e2e-test cypress - + CYPRESS_baseUrl: ${{steps.get-status-env.outputs.result}} + with: + config-file: ./packages/e2e-test/src/cypress.json + browser: chrome + headless: true + - name: set status if: ${{ failure() }} uses: actions/github-script@v3 diff --git a/packages/e2e-test/src/cypress/integration/catalog.ts b/packages/e2e-test/src/cypress/integration/catalog.ts index 3518bac68d..31d39e7efd 100644 --- a/packages/e2e-test/src/cypress/integration/catalog.ts +++ b/packages/e2e-test/src/cypress/integration/catalog.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/// import 'os'; describe('Catalog', () => { From 60108e4623f5f24fd78f3bf04b35b6b743d8971a Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 21:19:25 +0100 Subject: [PATCH 060/187] chore: reconfigure Cypress run --- .github/workflows/tugboat.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 8478701957..13374f9cc0 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -115,7 +115,8 @@ jobs: env: CYPRESS_baseUrl: ${{steps.get-status-env.outputs.result}} with: - config-file: ./packages/e2e-test/src/cypress.json + config-file: ./cypress.json + working-directory: ./packages/e2e-test/src browser: chrome headless: true From b533d651477b2443d1ef7a898881b8d9f28a35d1 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 21:39:31 +0100 Subject: [PATCH 061/187] chore: don't install - we have deps --- .github/workflows/tugboat.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 13374f9cc0..0836aa6059 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -118,6 +118,7 @@ jobs: config-file: ./cypress.json working-directory: ./packages/e2e-test/src browser: chrome + install: false headless: true - name: set status From e8cb51a71703d6315ef0dbaec1ea0ec6549f65e6 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 21:55:46 +0100 Subject: [PATCH 062/187] chore: dump video recordings too --- .github/workflows/tugboat.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 0836aa6059..7b7d6050de 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -120,6 +120,14 @@ jobs: browser: chrome install: false headless: true + + # Test run video was always captured, so this action uses "always()" condition + - uses: actions/upload-artifact@v1 + if: always() + with: + working-directory: ./packages/e2e-test/src + name: cypress-videos + path: cypress/videos - name: set status if: ${{ failure() }} From b8877af3059a7fe3fcc9718f7ec66e222e84c51c Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 21:58:21 +0100 Subject: [PATCH 063/187] chore: slimming down install script a little --- .github/workflows/tugboat.yml | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 7b7d6050de..45c91dfcce 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -53,37 +53,9 @@ jobs: with: node-version: '14' - name: fetch branch master - run: git fetch origin master - - # Cache every node_modules folder inside the monorepo - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v2 - with: - path: '**/node_modules' - # We use both yarn.lock and package.json as cache keys to ensure that - # changes to local monorepo packages bust the cache. - key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - - # If we get a cache hit for node_modules, there's no need to bring in the global - # yarn cache or run yarn install, as all dependencies will be installed already. - - name: find location of global yarn cache - id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" - - - name: cache global yarn cache - uses: actions/cache@v2 - if: steps.cache-modules.outputs.cache-hit != 'true' - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - + - name: yarn install - if: steps.cache-modules.outputs.cache-hit != 'true' - run: yarn install --frozen-lockfile + run: yarn workspace e2e-test install # This is required because the environment_url param that Tugboat uses # to tell us where the preview is located isn't supported unless you @@ -110,7 +82,7 @@ jobs: console.log(result); return result.data.environment_url; - - name: Cypress run + - name: cypress run uses: cypress-io/github-action@v2 env: CYPRESS_baseUrl: ${{steps.get-status-env.outputs.result}} From a54ea9e4f9e1b8a3a851ff653a36c3bfa34ff483 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 21:58:58 +0100 Subject: [PATCH 064/187] chore: make pretty --- .github/workflows/tugboat.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 45c91dfcce..ea275331ad 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -53,7 +53,7 @@ jobs: with: node-version: '14' - name: fetch branch master - + - name: yarn install run: yarn workspace e2e-test install @@ -93,14 +93,14 @@ jobs: install: false headless: true - # Test run video was always captured, so this action uses "always()" condition + # Test run video was always captured, so this action uses "always()" condition - uses: actions/upload-artifact@v1 if: always() with: working-directory: ./packages/e2e-test/src name: cypress-videos path: cypress/videos - + - name: set status if: ${{ failure() }} uses: actions/github-script@v3 @@ -115,7 +115,7 @@ jobs: context: 'Backstage Tugboat E2E Tests', target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); - + - name: set status if: ${{ success() }} uses: actions/github-script@v3 From d24ff519fa435a02efb8183f68bed95810ee99b3 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 22:08:31 +0100 Subject: [PATCH 065/187] chore: fixing syntax --- .github/workflows/tugboat.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index ea275331ad..df409c56cc 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -93,9 +93,9 @@ jobs: install: false headless: true - # Test run video was always captured, so this action uses "always()" condition - - uses: actions/upload-artifact@v1 - if: always() + - name: update artifact + if: ${{ always() }} + uses: actions/upload-artifact@v1 with: working-directory: ./packages/e2e-test/src name: cypress-videos From 31d38ee806c71007d738cd60b2be36fe665400ad Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 4 Feb 2021 22:09:24 +0100 Subject: [PATCH 066/187] chore: fixing syntax again --- .github/workflows/tugboat.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index df409c56cc..87a703b617 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -52,7 +52,6 @@ jobs: - uses: actions/setup-node@v1 with: node-version: '14' - - name: fetch branch master - name: yarn install run: yarn workspace e2e-test install From a435fefa61fcab8602cfd81aa475b7d669921429 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 5 Feb 2021 00:16:07 -0500 Subject: [PATCH 067/187] Align card elements with list --- .../User/UserProfileCard/UserProfileCard.tsx | 73 ++++++++++--------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index bdd0cd15f6..086979a0b4 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -20,7 +20,17 @@ import { } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; import { entityRouteParams } from '@backstage/plugin-catalog-react'; -import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; +import { + Box, + Grid, + Link, + List, + ListItem, + ListItemIcon, + ListItemText, + Tooltip, + Typography, +} from '@material-ui/core'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; import PersonIcon from '@material-ui/icons/Person'; @@ -80,40 +90,35 @@ export const UserProfileCard = ({ return User not found; } + const emailHref = profile?.email ? `mailto:${profile.email}` : ''; + return ( } variant={variant}> - - - - - + + + - - {profile?.email && ( - - - - - - - {profile.email} - - - - )} - - - - - - + + + {profile?.email && ( + + + + + + {profile.email} + + + )} + + + + + + + + {groupNames.map((groupName, index) => ( ))} - - - + + +
    From 8b7a19cbf278bfd0365b1143dd479d356701f2f8 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 5 Feb 2021 00:16:29 -0500 Subject: [PATCH 068/187] Align card elements with List --- .../Group/GroupProfile/GroupProfileCard.tsx | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 506a98d012..5ebce80c43 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -22,7 +22,16 @@ import { } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; import { entityRouteParams } from '@backstage/plugin-catalog-react'; -import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; +import { + Box, + Grid, + Link, + List, + ListItem, + ListItemIcon, + ListItemText, + Tooltip, +} from '@material-ui/core'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; @@ -76,66 +85,57 @@ export const GroupProfileCard = ({ ?.map(groupItem => groupItem.target.name) .toString(); - const childrens = group?.relations + const childRelations = group?.relations ?.filter(r => r.type === RELATION_PARENT_OF) ?.map(groupItem => groupItem.target.name); const displayName = profile?.displayName ?? name; + const descriptionHeading = description ?? name; if (!group) return User not found; return ( } - subheader={description} + title={} variant={variant} > - - - + - {profile?.email && ( - - - - - - - - {profile.email} - - - - )} - {parent ? ( - - - - - - + + {profile?.email && ( + + + + + + + {profile.email} + + )} + {parent ? ( + + + + + + + - - - - ) : null} - {childrens?.length ? ( - - - - - - - {childrens.map((children, index) => ( + + + ) : null} + {childRelations?.length ? ( + + + + + + + + {childRelations.map((children, index) => ( ))} - - - - ) : null} + + + ) : null} + From 14aef4b94278b7fa370adc2fef06a37eca1a2a1c Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 5 Feb 2021 00:19:26 -0500 Subject: [PATCH 069/187] Add changeset --- .changeset/nine-trees-thank.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nine-trees-thank.md diff --git a/.changeset/nine-trees-thank.md b/.changeset/nine-trees-thank.md new file mode 100644 index 0000000000..08847093e9 --- /dev/null +++ b/.changeset/nine-trees-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Visual updates to User and Group pages From afd496ec0155ae0d1befe3bef7cc3d84b0e386f8 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 5 Feb 2021 00:35:00 -0500 Subject: [PATCH 070/187] Removed unused import --- .../components/Cards/User/UserProfileCard/UserProfileCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index 086979a0b4..95c5db7f21 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -29,7 +29,6 @@ import { ListItemIcon, ListItemText, Tooltip, - Typography, } from '@material-ui/core'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; From c3f65bc445dc9a176f80714941840d8a8187c22c Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 5 Feb 2021 07:40:52 +0100 Subject: [PATCH 071/187] chore: archive video footage with the correct path --- .github/workflows/tugboat.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 87a703b617..4aedb9444c 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -96,9 +96,8 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v1 with: - working-directory: ./packages/e2e-test/src name: cypress-videos - path: cypress/videos + path: ./packages/e2e-test/src/cypress/videos - name: set status if: ${{ failure() }} From 93dd36999d9237336f1f47b7ca2d3650feb77dfd Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 5 Feb 2021 10:35:24 +0100 Subject: [PATCH 072/187] chore: fix lint warnings --- packages/e2e-test/.eslintrc.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/e2e-test/.eslintrc.js b/packages/e2e-test/.eslintrc.js index ce2592e03b..44427c5059 100644 --- a/packages/e2e-test/.eslintrc.js +++ b/packages/e2e-test/.eslintrc.js @@ -1,6 +1,5 @@ module.exports = { extends: [require.resolve('@backstage/cli/config/eslint.backend')], - ignorePatterns: ['templates/**'], rules: { 'no-console': 0, 'import/no-extraneous-dependencies': [ @@ -13,4 +12,19 @@ module.exports = { }, ], }, + + overrides: [ + { + files: ['**/cypress/integration/**'], + rules: { + 'jest/expect-expect': 'off' + }, + }, + { + files: ['src/commands/cypress.ts'], + rules: { + 'no-restricted-syntax': 'off' + } + } + ] }; From b3790710d6473575838d710f4bb6b63b0212b654 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 5 Feb 2021 10:44:22 +0100 Subject: [PATCH 073/187] chore: fix prettier again --- packages/e2e-test/.eslintrc.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/e2e-test/.eslintrc.js b/packages/e2e-test/.eslintrc.js index 44427c5059..2c41ed2cd3 100644 --- a/packages/e2e-test/.eslintrc.js +++ b/packages/e2e-test/.eslintrc.js @@ -17,14 +17,14 @@ module.exports = { { files: ['**/cypress/integration/**'], rules: { - 'jest/expect-expect': 'off' + 'jest/expect-expect': 'off', }, }, { files: ['src/commands/cypress.ts'], rules: { - 'no-restricted-syntax': 'off' - } - } - ] + 'no-restricted-syntax': 'off', + }, + }, + ], }; From a94f5f9506a80cbe9798932f01352e4d224073c8 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 5 Feb 2021 10:46:18 +0100 Subject: [PATCH 074/187] chore: move files around for ease --- .github/workflows/tugboat.yml | 4 ++-- packages/e2e-test/cypress.json | 8 ++++++++ packages/e2e-test/src/cypress.json | 8 -------- 3 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 packages/e2e-test/cypress.json delete mode 100644 packages/e2e-test/src/cypress.json diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 4aedb9444c..17d3b315d0 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -87,7 +87,7 @@ jobs: CYPRESS_baseUrl: ${{steps.get-status-env.outputs.result}} with: config-file: ./cypress.json - working-directory: ./packages/e2e-test/src + working-directory: ./packages/e2e-test browser: chrome install: false headless: true @@ -97,7 +97,7 @@ jobs: uses: actions/upload-artifact@v1 with: name: cypress-videos - path: ./packages/e2e-test/src/cypress/videos + path: ./packages/e2e-test/cypress/videos - name: set status if: ${{ failure() }} diff --git a/packages/e2e-test/cypress.json b/packages/e2e-test/cypress.json new file mode 100644 index 0000000000..22ea4a94ca --- /dev/null +++ b/packages/e2e-test/cypress.json @@ -0,0 +1,8 @@ +{ + "baseUrl": "http://localhost:7000", + "integrationFolder": "./src/cypress/integration", + "supportFile": "./src/cypress/support", + "fixturesFolder": "./src/cypress/fixures", + "pluginFile": "./src/cypress/plugins", + "defaultCommandTimeout": 10000 +} diff --git a/packages/e2e-test/src/cypress.json b/packages/e2e-test/src/cypress.json deleted file mode 100644 index 54b24ebbf0..0000000000 --- a/packages/e2e-test/src/cypress.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "baseUrl": "http://localhost:7000", - "integrationFolder": "./cypress/integration", - "supportFile": "./cypress/support", - "fixturesFolder": "./cypress/fixures", - "pluginFile": "./cypress/plugins", - "defaultCommandTimeout": 10000 -} From 09c1cca9858a0ed514d3555c2171534c16127837 Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Fri, 5 Feb 2021 11:22:51 +0100 Subject: [PATCH 075/187] Added pattern for component name --- .../sample-templates/docs-template/template.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml index 36404f088c..168daffd20 100644 --- a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml @@ -22,6 +22,7 @@ spec: component_id: title: Name type: string + pattern: ^[a-z0-9A-Z_.-]{1,63}$ description: Unique name of the component description: title: Description From a5005699900714737aa9104f2aed6fb4c2ce6e53 Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Fri, 5 Feb 2021 11:35:04 +0100 Subject: [PATCH 076/187] Add changeset --- .changeset/fifty-experts-exist.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fifty-experts-exist.md diff --git a/.changeset/fifty-experts-exist.md b/.changeset/fifty-experts-exist.md new file mode 100644 index 0000000000..63834e0f3e --- /dev/null +++ b/.changeset/fifty-experts-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add pattern to validate component_id for docs-template. #4216 From be6c650831d0e450e66681d9c4d6c9e60a81c1c4 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 5 Feb 2021 12:09:03 +0100 Subject: [PATCH 077/187] stash: push to stash --- packages/e2e-test/package.json | 1 - packages/e2e-test/src/cypress/tsconfig.json | 0 packages/e2e-test/tsconfig.json | 38 +++++++++++++++++++++ yarn.lock | 9 +---- 4 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 packages/e2e-test/src/cypress/tsconfig.json create mode 100644 packages/e2e-test/tsconfig.json diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 828788d7a4..9bcae01533 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -43,7 +43,6 @@ "ext": "ts" }, "dependencies": { - "@types/cypress": "^1.1.3", "cypress": "^6.4.0" } } diff --git a/packages/e2e-test/src/cypress/tsconfig.json b/packages/e2e-test/src/cypress/tsconfig.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/e2e-test/tsconfig.json b/packages/e2e-test/tsconfig.json new file mode 100644 index 0000000000..8217a7ec47 --- /dev/null +++ b/packages/e2e-test/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "allowJs": true, + "declaration": true, + "declarationMap": false, + "emitDeclarationOnly": true, + "esModuleInterop": true, + "experimentalDecorators": false, + "forceConsistentCasingInFileNames": true, + "importHelpers": false, + "incremental": true, + "isolatedModules": true, + "jsx": "react", + "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020", "ESNext.Promise"], + "module": "ESNext", + "moduleResolution": "node", + "noEmit": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "pretty": true, + "removeComments": false, + "resolveJsonModule": true, + "sourceMap": false, + "skipLibCheck": true, + "strict": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2019", + "types": ["node", "cypress", "webpack-env"] + } +} diff --git a/yarn.lock b/yarn.lock index 1c328d8a93..060205f11e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6618,13 +6618,6 @@ dependencies: postcss "5 - 7" -"@types/cypress@^1.1.3": - version "1.1.3" - resolved "https://registry.npmjs.org/@types/cypress/-/cypress-1.1.3.tgz#0a700c040d53e9e12b5af98e41d4a88c39f39b6a" - integrity sha512-OXe0Gw8LeCflkG1oPgFpyrYWJmEKqYncBsD/J0r17r0ETx/TnIGDNLwXt/pFYSYuYTpzcq1q3g62M9DrfsBL4g== - dependencies: - cypress "*" - "@types/d3-color@*": version "2.0.0" resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.0.tgz#febdfadade56e215a4c3f612fe3000d92999f5d5" @@ -11513,7 +11506,7 @@ cyclist@^1.0.1: resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress@*, cypress@^6.4.0: +cypress@^6.4.0: version "6.4.0" resolved "https://registry.npmjs.org/cypress/-/cypress-6.4.0.tgz#432c516bf4f1a0f042a6aa1f2c3a4278fa35a8b2" integrity sha512-SrsPsZ4IBterudkoFYBvkQmXOVxclh1/+ytbzpV8AH/D2FA+s2Qy5ISsaRzOFsbQa4KZWoi3AKwREmF1HucYkg== From e75932117f1f18a27d044fa12c859a40fb5119dd Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 5 Feb 2021 09:54:19 -0500 Subject: [PATCH 078/187] Return AboutCard title to group name --- .../components/Cards/Group/GroupProfile/GroupProfileCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 5ebce80c43..c43f118b80 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -90,13 +90,13 @@ export const GroupProfileCard = ({ ?.map(groupItem => groupItem.target.name); const displayName = profile?.displayName ?? name; - const descriptionHeading = description ?? name; if (!group) return User not found; return ( } + title={} + subheader={description} variant={variant} > From 05fa2dcf98e67fa5e5db14e67d8b6b23a39e2599 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 5 Feb 2021 12:32:35 -0500 Subject: [PATCH 079/187] Refactor children to use shared function --- .../Cards/Group/GroupProfile/GroupProfileCard.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index c43f118b80..3d7d53d52a 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -21,7 +21,10 @@ import { RELATION_PARENT_OF, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { entityRouteParams } from '@backstage/plugin-catalog-react'; +import { + getEntityRelations, + entityRouteParams, +} from '@backstage/plugin-catalog-react'; import { Box, Grid, @@ -85,9 +88,9 @@ export const GroupProfileCard = ({ ?.map(groupItem => groupItem.target.name) .toString(); - const childRelations = group?.relations - ?.filter(r => r.type === RELATION_PARENT_OF) - ?.map(groupItem => groupItem.target.name); + const childRelations = getEntityRelations(group, RELATION_PARENT_OF, { + kind: 'group', + }); const displayName = profile?.displayName ?? name; @@ -137,10 +140,9 @@ export const GroupProfileCard = ({ {childRelations.map((children, index) => ( ))} From f4b576d0e2d17580115fc2cee8bed95bc7d1c14f Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sat, 6 Feb 2021 14:19:34 +0100 Subject: [PATCH 080/187] TechDocs: Add visbility to migrate away from basic setup --- .changeset/metal-monkeys-do.md | 5 +++++ app-config.yaml | 3 +++ .../create-app/templates/default-app/app-config.yaml.hbs | 3 +++ 3 files changed, 11 insertions(+) create mode 100644 .changeset/metal-monkeys-do.md diff --git a/.changeset/metal-monkeys-do.md b/.changeset/metal-monkeys-do.md new file mode 100644 index 0000000000..372d6f472f --- /dev/null +++ b/.changeset/metal-monkeys-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +TechDocs: Add comments about migrating away from basic setup in app-config.yaml diff --git a/app-config.yaml b/app-config.yaml index 5240a050a8..0e88c91371 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -73,6 +73,9 @@ organization: name: My Company # Reference documentation http://backstage.io/docs/features/techdocs/configuration +# Note: After experimenting with basic setup, use CI/CD to generate docs +# and an external cloud storage when deploying TechDocs for production use-case. +# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach techdocs: builder: 'local' # Alternatives - 'external' generators: diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 495b30dd6f..f8a345cd1e 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -57,6 +57,9 @@ proxy: changeOrigin: true # Reference documentation http://backstage.io/docs/features/techdocs/configuration +# Note: After experimenting with basic setup, use CI/CD to generate docs +# and an external cloud storage when deploying TechDocs for production use-case. +# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach techdocs: builder: 'local' # Alternatives - 'external' generators: From ca559171bf639d0ed1cf2c36df83197771d63825 Mon Sep 17 00:00:00 2001 From: NHI TRAN Date: Fri, 5 Feb 2021 22:05:29 -0500 Subject: [PATCH 081/187] bugfix: [3310] Favoriting a component resets interface to your owned components update changeset remove unused import fix changeset message fix vale errors? --- .changeset/giant-kids-hear.md | 5 +++++ .../CatalogFilter/CatalogFilter.tsx | 8 +++++++ .../CatalogPage/CatalogPage.test.tsx | 22 ++++++++++++++++++- .../components/CatalogPage/CatalogPage.tsx | 18 ++++++++++----- .../src/filter/useEntityFilterGroup.ts | 10 ++++++--- 5 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 .changeset/giant-kids-hear.md diff --git a/.changeset/giant-kids-hear.md b/.changeset/giant-kids-hear.md new file mode 100644 index 0000000000..8bfb3ea8b2 --- /dev/null +++ b/.changeset/giant-kids-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +bug fix: 3310 fixes reloading entities with the default owned filter diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index 90a1e15ad0..6de4c318b5 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -80,6 +80,14 @@ type Props = { onChange?: OnChangeCallback; }; +/** + * Sidebar filter type and human readable label for it. owned/starred/all + */ +export type CatalogFilterType = { + id: string; + label: string; +}; + /** * The main filter group in the sidebar, toggling owned/starred/all. */ diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 42c9cf1117..6d4d18e4f2 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -30,7 +30,7 @@ import { } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { EntityFilterGroupsProvider } from '../../filter'; import { CatalogPage } from './CatalogPage'; @@ -128,4 +128,24 @@ describe('CatalogPage', () => { fireEvent.click(getByText(/All/)); expect(await findByText(/All \(2\)/)).toBeInTheDocument(); }); + // this test is for fixing the bug after favoriting an entity, the matching entities defaulting + // to "owned" filter and not based on the selected filter + it('should render the correct entities filtered on the selectedfilter', async () => { + const { findByText, findAllByTitle, getByText } = renderWrapped( + , + ); + expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(await findByText(/Starred/)).toBeInTheDocument(); + fireEvent.click(getByText(/Starred/)); + expect(await findByText(/Starred \(0\)/)).toBeInTheDocument(); + fireEvent.click(getByText(/All/)); + expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + + const starredIcons = await findAllByTitle('Add to favorites'); + fireEvent.click(starredIcons[0]); + expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + + fireEvent.click(getByText(/Starred/)); + waitFor(() => expect(findByText(/Starred \(1\)/)).toBeInTheDocument()); + }); }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 1298d5e190..a2ebc63825 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -31,7 +31,11 @@ import React, { useCallback, useMemo, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; import { useStarredEntities } from '../../hooks/useStarredEntities'; -import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter'; +import { + ButtonGroup, + CatalogFilter, + CatalogFilterType, +} from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import { useOwnUser } from '../useOwnUser'; @@ -65,7 +69,9 @@ const CatalogPageContents = () => { const errorApi = useApi(errorApiRef); const { isStarredEntity } = useStarredEntities(); const [selectedTab, setSelectedTab] = useState(); - const [selectedSidebarItem, setSelectedSidebarItem] = useState(); + const [selectedSidebarItem, setSelectedSidebarItem] = useState< + CatalogFilterType + >(); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; const addMockData = useCallback(async () => { @@ -180,13 +186,15 @@ const CatalogPageContents = () => {
    setSelectedSidebarItem(label)} - initiallySelected="owned" + onChange={({ label, id }) => + setSelectedSidebarItem({ label, id }) + } + initiallySelected={selectedSidebarItem?.id ?? 'owned'} />
    initialSelectedFilters?.slice(), []); + const initialMemo = useMemo(() => { + return initialSelectedFilters?.slice(); + }, [initialSelectedFilters]); // Register the group on mount, and unregister on unmount useEffect(() => { register(filterGroupId, filterGroup, initialMemo); return () => unregister(filterGroupId); - }, [register, unregister, filterGroupId, filterGroup, initialMemo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [register, unregister, filterGroupId, filterGroup]); const setSelectedFilters = useCallback( (filters: string[]) => { From 529c8501b822aa14ddef4d44a75cf3f2f75f900d Mon Sep 17 00:00:00 2001 From: Nils Streijffert Date: Mon, 8 Feb 2021 09:51:57 +0100 Subject: [PATCH 082/187] Create utc DateTime object directly --- plugins/auth-backend/src/identity/MemoryKeyStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/identity/MemoryKeyStore.ts b/plugins/auth-backend/src/identity/MemoryKeyStore.ts index 0903f6987d..ecb31d9998 100644 --- a/plugins/auth-backend/src/identity/MemoryKeyStore.ts +++ b/plugins/auth-backend/src/identity/MemoryKeyStore.ts @@ -25,7 +25,7 @@ export class MemoryKeyStore implements KeyStore { async addKey(key: AnyJWK): Promise { this.keys.set(key.kid, { - createdAt: DateTime.local().toUTC(), + createdAt: DateTime.utc(), key: JSON.stringify(key), }); } From ffffea8e6d2dccc312343192bfbe26fe095bb292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 8 Feb 2021 10:52:10 +0100 Subject: [PATCH 083/187] integration: update the gitlab config mandatory fields to match reality --- .changeset/great-waves-reflect.md | 6 +++ .changeset/pink-kids-bake.md | 31 +++++++++++++ .../src/reading/GitlabUrlReader.test.ts | 14 +----- .../src/reading/GitlabUrlReader.ts | 6 --- .../src/gitlab/GitLabIntegration.test.ts | 2 + .../integration/src/gitlab/config.test.ts | 10 +++- packages/integration/src/gitlab/config.ts | 46 ++++++++++++++----- packages/integration/src/gitlab/core.test.ts | 4 ++ packages/integration/src/helpers.ts | 19 ++++++-- .../scaffolder/stages/prepare/gitlab.test.ts | 4 +- .../scaffolder/stages/publish/gitlab.test.ts | 3 ++ .../scaffolder/stages/publish/publishers.ts | 2 + 12 files changed, 111 insertions(+), 36 deletions(-) create mode 100644 .changeset/great-waves-reflect.md create mode 100644 .changeset/pink-kids-bake.md diff --git a/.changeset/great-waves-reflect.md b/.changeset/great-waves-reflect.md new file mode 100644 index 0000000000..9304a51deb --- /dev/null +++ b/.changeset/great-waves-reflect.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Minor updates to reflect the changes in `@backstage/integration` that made the fields `apiBaseUrl` and `apiUrl` mandatory. diff --git a/.changeset/pink-kids-bake.md b/.changeset/pink-kids-bake.md new file mode 100644 index 0000000000..c0bd846ff1 --- /dev/null +++ b/.changeset/pink-kids-bake.md @@ -0,0 +1,31 @@ +--- +'@backstage/integration': minor +--- + +Update the `GitLabIntegrationConfig` to require the fields `apiBaseUrl` and `baseUrl`. The `readGitLabIntegrationConfig` function is now more strict and has better error reporting. This change mirrors actual reality in code more properly - the fields are actually necessary for many parts of code to actually function, so they should no longer be optional. + +Some checks that used to happen deep inside code that consumed config, now happen upfront at startup. This means that you may encounter new errors at backend startup, if you had actual mistakes in config but didn't happen to exercise the code paths that actually would break. But for most users, no change will be necessary. + +An example minimal GitLab config block that just adds a token to public GitLab would look similar to this: + +```yaml +integrations: + gitlab: + - host: gitlab.com + token: + $env: GITLAB_TOKEN +``` + +A full fledged config that points to a locally hosted GitLab could look like this: + +```yaml +integrations: + gitlab: + - host: gitlab.my-company.com + apiBaseUrl: https://gitlab.my-company.com/api/v4 + baseUrl: https://gitlab.my-company.com + token: + $env: OUR_GITLAB_TOKEN +``` + +In this case, the only optional field is `baseUrl` which is formed from the `host` if needed. diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index b18e7a4294..5b0b4002a3 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -36,6 +36,7 @@ const gitlabProcessor = new GitlabUrlReader( { host: 'gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', + baseUrl: 'https://gitlab.com', }, { treeResponseFactory }, ); @@ -44,6 +45,7 @@ const hostedGitlabProcessor = new GitlabUrlReader( { host: 'gitlab.mycompany.com', apiBaseUrl: 'https://gitlab.mycompany.com/api/v4', + baseUrl: 'https://gitlab.mycompany.com', }, { treeResponseFactory }, ); @@ -379,17 +381,5 @@ describe('GitlabUrlReader', () => { }; await expect(fnGithub).rejects.toThrow(NotFoundError); }); - - it('should throw error when apiBaseUrl is missing', () => { - expect(() => { - /* eslint-disable no-new */ - new GitlabUrlReader( - { - host: 'gitlab.mycompany.com', - }, - { treeResponseFactory }, - ); - }).toThrowError('must configure an explicit apiBaseUrl'); - }); }); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 8a763d1259..22316f6895 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -51,12 +51,6 @@ export class GitlabUrlReader implements UrlReader { deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { this.treeResponseFactory = deps.treeResponseFactory; - - if (!config.apiBaseUrl) { - throw new Error( - `GitLab integration for '${config.host}' must configure an explicit apiBaseUrl`, - ); - } } async read(url: string): Promise { diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts index 8814e33302..6faae3a5e7 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.test.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -26,6 +26,8 @@ describe('GitLabIntegration', () => { { host: 'h.com', token: 't', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', }, ], }, diff --git a/packages/integration/src/gitlab/config.test.ts b/packages/integration/src/gitlab/config.test.ts index 303b4bd270..bfa59d001e 100644 --- a/packages/integration/src/gitlab/config.test.ts +++ b/packages/integration/src/gitlab/config.test.ts @@ -31,6 +31,7 @@ describe('readGitLabIntegrationConfig', () => { buildConfig({ host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com', baseUrl: 'https://baseurl.for.me/gitlab', }), ); @@ -38,12 +39,15 @@ describe('readGitLabIntegrationConfig', () => { expect(output).toEqual({ host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com', baseUrl: 'https://baseurl.for.me/gitlab', }); }); it('inserts the defaults if missing', () => { - const output = readGitLabIntegrationConfig(buildConfig({})); + const output = readGitLabIntegrationConfig( + buildConfig({ host: 'gitlab.com' }), + ); expect(output).toEqual({ host: 'gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', @@ -88,12 +92,15 @@ describe('readGitLabIntegrationConfigs', () => { { host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com/api/v4', + baseUrl: 'https://a.com', }, ]), ); expect(output).toContainEqual({ host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com/api/v4', baseUrl: 'https://a.com', }); }); @@ -104,6 +111,7 @@ describe('readGitLabIntegrationConfigs', () => { { host: 'gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', + baseUrl: 'https://gitlab.com', }, ]); }); diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 47269b4c32..cf717e08d2 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { isValidHost } from '../helpers'; +import { isValidHost, isValidUrl } from '../helpers'; const GITLAB_HOST = 'gitlab.com'; const GITLAB_API_BASE_URL = 'https://gitlab.com/api/v4'; @@ -38,7 +38,7 @@ export type GitLabIntegrationConfig = { * The API will always be preferred if both its base URL and a token are * present. */ - apiBaseUrl?: string; + apiBaseUrl: string; /** * The authorization token to use for requests this provider. @@ -53,7 +53,7 @@ export type GitLabIntegrationConfig = { * * If no baseUrl is provided, it will default to https://${host} */ - baseUrl?: string; + baseUrl: string; }; /** @@ -64,16 +64,10 @@ export type GitLabIntegrationConfig = { export function readGitLabIntegrationConfig( config: Config, ): GitLabIntegrationConfig { - const host = config.getOptionalString('host') ?? GITLAB_HOST; + const host = config.getString('host'); let apiBaseUrl = config.getOptionalString('apiBaseUrl'); const token = config.getOptionalString('token'); - const baseUrl = config.getOptionalString('baseUrl') ?? `https://${host}`; - - if (!isValidHost(host)) { - throw new Error( - `Invalid GitLab integration config, '${host}' is not a valid host`, - ); - } + let baseUrl = config.getOptionalString('baseUrl'); if (apiBaseUrl) { apiBaseUrl = apiBaseUrl.replace(/\/+$/, ''); @@ -81,6 +75,30 @@ export function readGitLabIntegrationConfig( apiBaseUrl = GITLAB_API_BASE_URL; } + if (baseUrl) { + baseUrl = baseUrl.replace(/\/+$/, ''); + } else { + baseUrl = `https://${host}`; + } + + if (host.includes(':')) { + throw new Error( + `Invalid GitLab integration config, host '${host}' should just be the host name (e.g. "github.com"), not a URL`, + ); + } else if (!isValidHost(host)) { + throw new Error( + `Invalid GitLab integration config, '${host}' is not a valid host`, + ); + } else if (!apiBaseUrl || !isValidUrl(apiBaseUrl)) { + throw new Error( + `Invalid GitLab integration config, '${apiBaseUrl}' is not a valid apiBaseUrl`, + ); + } else if (!isValidUrl(baseUrl)) { + throw new Error( + `Invalid GitLab integration config, '${baseUrl}' is not a valid baseUrl`, + ); + } + return { host, token, apiBaseUrl, baseUrl }; } @@ -99,7 +117,11 @@ export function readGitLabIntegrationConfigs( // As a convenience we always make sure there's at least an unauthenticated // reader for public gitlab repos. if (!result.some(c => c.host === GITLAB_HOST)) { - result.push({ host: GITLAB_HOST, apiBaseUrl: GITLAB_API_BASE_URL }); + result.push({ + host: GITLAB_HOST, + apiBaseUrl: GITLAB_API_BASE_URL, + baseUrl: `https://${GITLAB_HOST}`, + }); } return result; diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index 2b4713604c..6836854eae 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -37,10 +37,14 @@ describe('gitlab core', () => { const configWithToken: GitLabIntegrationConfig = { host: 'g.com', token: '0123456789', + apiBaseUrl: '', + baseUrl: '', }; const configWithNoToken: GitLabIntegrationConfig = { host: 'g.com', + apiBaseUrl: '', + baseUrl: '', }; describe('getGitLabFileFetchUrl', () => { diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index cc1c59a238..73cef18297 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -16,11 +16,22 @@ import { ScmIntegration, ScmIntegrationsGroup } from './types'; -/** Checks whether the given url is a valid host */ -export function isValidHost(url: string): boolean { +/** Checks whether the given argument is a valid URL hostname */ +export function isValidHost(host: string): boolean { const check = new URL('http://example.com'); - check.host = url; - return check.host === url; + check.host = host; + return check.host === host; +} + +/** Checks whether the given argument is a valid URL */ +export function isValidUrl(url: string): boolean { + try { + // eslint-disable-next-line no-new + new URL(url); + return true; + } catch { + return false; + } } export function basicIntegrations( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index 6ed4651e4f..2de2326505 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -37,8 +37,10 @@ describe('GitLabPreparer', () => { jest.clearAllMocks(); }); const preparer = GitlabPreparer.fromConfig({ - host: 'gitlab.com', + host: '', token: 'fake-token', + apiBaseUrl: '', + baseUrl: '', }); it(`calls the clone command with the correct arguments for a repository`, async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts index 61c8480efb..3826a54ddb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -57,6 +57,7 @@ describe('GitLab Publisher', () => { const publisher = await GitlabPublisher.fromConfig( { host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', token: 'fake-token', baseUrl: 'https://gitlab.hosted.com', }, @@ -105,7 +106,9 @@ describe('GitLab Publisher', () => { const publisher = await GitlabPublisher.fromConfig( { host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', token: 'fake-token', + baseUrl: 'https://gitlab.com', }, { repoVisibility: 'public' }, ); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 30201e6dca..f5f7fc6361 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -126,6 +126,8 @@ export class Publishers implements PublisherBuilder { { token: config.getOptionalString('scaffolder.gitlab.token') ?? '', host: integration.config.host, + apiBaseUrl: ``, + baseUrl: `https://${integration.config.host}`, }, { repoVisibility }, ), From 6a8b219e6c9382ede2a2cff2ae7a16d0a4b6f1ea Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Fri, 5 Feb 2021 19:01:50 +0100 Subject: [PATCH 084/187] Resolve review comments --- packages/app/src/App.tsx | 4 +- plugins/catalog-import/README.md | 20 +- plugins/catalog-import/dev/index.tsx | 127 +++++++- plugins/catalog-import/package.json | 1 - .../src/api/CatalogImportApi.ts | 6 +- .../src/api/CatalogImportClient.test.ts | 1 - .../src/api/CatalogImportClient.ts | 6 +- plugins/catalog-import/src/api/GitHub.ts | 17 +- .../src/components/Buttons/index.tsx | 5 +- .../EntityListComponent.tsx | 26 +- .../src/components/ImportComponentPage.tsx | 9 +- .../ImportStepper/ImportStepper.tsx | 16 +- .../src/components/ImportStepper/defaults.tsx | 4 +- .../StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx | 5 +- .../AutocompleteTextField.tsx | 2 +- .../StepPrepareCreatePullRequest.test.tsx | 1 - .../StepPrepareCreatePullRequest.tsx | 10 +- .../StepReviewLocation/StepReviewLocation.tsx | 2 +- .../src/components/useImportState.test.tsx | 56 ++-- .../src/components/useImportState.ts | 304 ++++++++++-------- plugins/catalog-import/src/setupTests.ts | 1 + plugins/catalog-import/src/types.ts | 27 ++ 22 files changed, 416 insertions(+), 234 deletions(-) create mode 100644 plugins/catalog-import/src/types.ts diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 5910d4674c..49d5f09fcf 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -35,7 +35,7 @@ import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse'; import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component'; import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; -import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; import { Route, Navigate } from 'react-router'; import { EntityPage } from './components/catalog/EntityPage'; @@ -69,7 +69,7 @@ const catalogRouteRef = createRouteRef({ const routes = ( - } /> + } /> } diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index c9583eb874..3befeb7918 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -7,7 +7,7 @@ It also assists by creating pull requests in repositories where no `catalog-info Current features: -- Import `catalog-info.yaml` files from a URL in a repository of one of the supported Git integrations (example `https://github.com/backstage/backstage/catalog.info`). +- Import `catalog-info.yaml` files from a URL in a repository of one of the supported Git integrations (example `https://github.com/backstage/backstage/catalog-info.yaml`). - _[GitHub only]_ Search for all `catalog-info.yaml` files in a Git repository (example: `https://github.com/backstage/backstage`). - _[GitHub only]_ Analyze a repository, generate a Component entity, and create a Pull Request to onboard the repository. @@ -28,40 +28,38 @@ yarn add @backstage/plugin-catalog-import ```ts // packages/app/src/plugins.ts -export { plugin as CatalogImportPlugin } from '@backstage/plugin-catalog-import'; +export { catalogImportPlugin } from '@backstage/plugin-catalog-import'; ``` -3. Register the `ImportComponentRouter` at the `/catalog-import` path: +3. Register the `CatalogImportPage` at the `/catalog-import` path: ```tsx // packages/app/src/App.tsx -import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; -} />; +} />; ``` ## Customizations ### Disable the creation of Pull Requests -The pull request feature can be disabled by options that are passed to the `ImportComponentRouter`: +The pull request feature can be disabled by options that are passed to the `CatalogImportPage`: ```tsx // packages/app/src/App.tsx - } + element={} /> ``` ### Customize the title and body of the Pull Request The pull request form is filled with a default title and body. -This can be configured by options that are passed to the `ImportComponentRouter`: +This can be configured by options that are passed to the `CatalogImportPage`: ```tsx // packages/app/src/App.tsx @@ -69,7 +67,7 @@ This can be configured by options that are passed to the `ImportComponentRouter` ({ diff --git a/plugins/catalog-import/dev/index.tsx b/plugins/catalog-import/dev/index.tsx index fc0a4c6c1b..e794628e6c 100644 --- a/plugins/catalog-import/dev/index.tsx +++ b/plugins/catalog-import/dev/index.tsx @@ -16,15 +16,18 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity, EntityName } from '@backstage/catalog-model'; -import { Content, Header, Page } from '@backstage/core'; +import { Content, Header, InfoCard, Page } from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { Grid } from '@material-ui/core'; +import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; +import AlarmIcon from '@material-ui/icons/Alarm'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; import React from 'react'; import { AnalyzeResult, CatalogImportApi, catalogImportApiRef, + EntityListComponent, ImportStepper, } from '../src'; import { ImportComponentPage } from '../src/components/ImportComponentPage'; @@ -61,6 +64,59 @@ const getEntities = (url: string): Entity[] => [ }, ]; +const locations = [ + { + target: 'https://my-location-1', + entities: [ + { + kind: 'Domain', + namespace: 'default', + name: 'my-domain', + }, + { + kind: 'Group', + namespace: 'groups', + name: 'my-group', + }, + { + kind: 'Location', + namespace: 'default', + name: 'my-location', + }, + { + kind: 'System', + namespace: 'default', + name: 'my-system', + }, + { + kind: 'User', + namespace: 'users', + name: 'my-api', + }, + ], + }, + { + target: 'https://my-location-2', + entities: [ + { + kind: 'API', + namespace: 'default', + name: 'my-api', + }, + { + kind: 'Component', + namespace: 'default', + name: 'my-component', + }, + { + kind: 'Location', + namespace: 'default', + name: 'my-location', + }, + ], + }, +]; + createDevApp() .registerApi({ api: catalogApiRef, @@ -225,4 +281,71 @@ createDevApp() ), }) + .addPage({ + title: 'Components', + element: ( + +
    + + + + + } + /> + + + + + + + + + + + } + locations={locations} + locationListItemIcon={() => } + onItemClick={() => {}} + /> + + + + + } + /> + + + + + } + withLinks + /> + + + + + + ), + }) .render(); diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index c35b2b7bde..ab02da8798 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -41,7 +41,6 @@ "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.0.12", "@types/react": "^16.9", - "cross-fetch": "^3.0.6", "git-url-parse": "^11.4.4", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 7a72bcddfe..5237e0339f 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { EntityName } from '@backstage/catalog-model'; import { createApiRef } from '@backstage/core'; +import { PartialEntity } from '../types'; export const catalogImportApiRef = createApiRef({ id: 'plugin.catalog-import.service', @@ -35,7 +36,7 @@ export type AnalyzeResult = type: 'repository'; url: string; integrationType: string; - generatedEntities: Entity[]; + generatedEntities: PartialEntity[]; }; export interface CatalogImportApi { @@ -43,7 +44,6 @@ export interface CatalogImportApi { submitPullRequest(options: { repositoryUrl: string; - integrationType: string; fileContent: string; title: string; body: string; diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index cefe74e8ba..324c4773b2 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -284,7 +284,6 @@ describe('CatalogImportClient', () => { await expect( catalogImportClient.submitPullRequest({ - integrationType: 'github', repositoryUrl: 'https://github.com/backstage/backstage', fileContent: 'some content', title: 'A title', diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 1cdd726a9a..265a3d3946 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -19,7 +19,6 @@ import { Entity, EntityName } from '@backstage/catalog-model'; import { ConfigApi, DiscoveryApi, OAuthApi } from '@backstage/core'; import { GitHubIntegrationConfig } from '@backstage/integration'; import { Octokit } from '@octokit/rest'; -import fetch from 'cross-fetch'; import { AnalyzeResult, CatalogImportApi } from './CatalogImportApi'; import { getGithubIntegrationConfig } from './GitHub'; @@ -94,20 +93,19 @@ export class CatalogImportClient implements CatalogImportApi { } async submitPullRequest({ - integrationType, repositoryUrl, fileContent, title, body, }: { repositoryUrl: string; - integrationType: string; fileContent: string; title: string; body: string; }): Promise<{ link: string; location: string }> { const ghConfig = getGithubIntegrationConfig(this.configApi, repositoryUrl); - if (integrationType === 'github' && ghConfig) { + + if (ghConfig) { return await this.submitGitHubPrToRepo({ ...ghConfig, fileContent, diff --git a/plugins/catalog-import/src/api/GitHub.ts b/plugins/catalog-import/src/api/GitHub.ts index 7805e8fb59..6a798ec201 100644 --- a/plugins/catalog-import/src/api/GitHub.ts +++ b/plugins/catalog-import/src/api/GitHub.ts @@ -15,28 +15,25 @@ */ import { ConfigApi } from '@backstage/core'; - -// TODO: (O5ten) Refactor into a core API instead of direct usage like this -// https://github.com/backstage/backstage/pull/3613#issuecomment-7408929430 -import { readGitHubIntegrationConfigs } from '@backstage/integration'; +import { ScmIntegrations } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; export const getGithubIntegrationConfig = ( config: ConfigApi, location: string, ) => { - const { name: repo, owner, resource: hostname } = parseGitUrl(location); + const { name: repo, owner } = parseGitUrl(location); + + const scmIntegrations = ScmIntegrations.fromConfig(config); + const githubIntegrationConfig = scmIntegrations.github.byUrl(location); - const configs = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - const githubIntegrationConfig = configs.find(v => v.host === hostname); if (!githubIntegrationConfig) { return undefined; } + return { repo, owner, - githubIntegrationConfig, + githubIntegrationConfig: githubIntegrationConfig.config, }; }; diff --git a/plugins/catalog-import/src/components/Buttons/index.tsx b/plugins/catalog-import/src/components/Buttons/index.tsx index 18f968a32b..93dd96b399 100644 --- a/plugins/catalog-import/src/components/Buttons/index.tsx +++ b/plugins/catalog-import/src/components/Buttons/index.tsx @@ -40,6 +40,7 @@ const useStyles = makeStyles(theme => ({ export const NextButton = ( props: ComponentProps & { loading?: boolean }, ) => { + const { loading, ...buttonProps } = props; const classes = useStyles(); return ( @@ -47,11 +48,11 @@ export const NextButton = (