From b06f6767988416879d64db04a1ed344720e0cbc5 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 2 Jan 2021 14:50:45 +0100 Subject: [PATCH 01/44] feat: reworking some of the preparers to support the integration config and add deprecation for old scaffolder config --- packages/integration/src/gitlab/config.ts | 2 +- .../scaffolder/stages/prepare/azure.test.ts | 57 +++++++++++++------ .../src/scaffolder/stages/prepare/azure.ts | 53 +++++++++++++---- .../scaffolder/stages/prepare/github.test.ts | 52 ++++++++++++++--- .../src/scaffolder/stages/prepare/github.ts | 50 +++++++++++++--- .../scaffolder/stages/prepare/gitlab.test.ts | 27 ++++----- .../src/scaffolder/stages/prepare/gitlab.ts | 25 ++++++-- .../scaffolder/stages/prepare/preparers.ts | 24 ++------ .../src/scaffolder/stages/prepare/types.ts | 8 +-- 9 files changed, 209 insertions(+), 89 deletions(-) diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 2d9f4b39ab..28687399e1 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -31,7 +31,7 @@ export type GitLabIntegrationConfig = { /** * The base URL of the API of this provider, e.g. "https://gitlab.com/api/v4", - * with no trailing slash. + * with no trailing slash.s * * May be omitted specifically for GitLab; then it will be deduced. * diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 39efd1c62a..38c262eaaa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -33,6 +33,8 @@ describe('AzurePreparer', () => { clone: jest.fn(), }; + const logger = getVoidLogger(); + jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); let mockEntity: TemplateEntityV1alpha1; @@ -78,31 +80,55 @@ describe('AzurePreparer', () => { }; }); - it('initializes git client with the correct arguments if an access token is provided for a repository', async () => { + // TODO(blam): Here's a test that will fail when the deprecation is complete + it('calls the clone command with deprecated token', async () => { const preparer = new AzurePreparer( new ConfigReader({ scaffolder: { azure: { api: { - token: 'fake-token', + token: 'fake-azure-token', }, }, }, }), + { logger }, ); - const logger = getVoidLogger(); - await preparer.prepare(mockEntity, { logger }); + + await preparer.prepare(mockEntity); expect(Git.fromAuth).toHaveBeenCalledWith({ - username: 'notempty', - password: 'fake-token', logger, + password: 'fake-azure-token', + username: 'notempty', }); }); - it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + it('calls the clone command with token from integrations config', async () => { + const preparer = new AzurePreparer( + new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'fake-azure-token-integration' }, + ], + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + password: 'fake-azure-token-integration', + username: 'notempty', + }); + }); + + it('calls the clone command with the correct arguments for a repository', async () => { + const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: @@ -112,10 +138,10 @@ describe('AzurePreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); + const preparer = new AzurePreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: @@ -125,12 +151,10 @@ describe('AzurePreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); + const preparer = new AzurePreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); + const response = await preparer.prepare(mockEntity); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, @@ -138,11 +162,10 @@ describe('AzurePreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); + const preparer = new AzurePreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), workingDirectory: '/workDir', }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index a38c5f4fdc..bb2257dfa7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -22,22 +22,46 @@ import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; import GitUriParser from 'git-url-parse'; import { Config } from '@backstage/config'; +import { Logger } from 'winston'; +import { + readAzureIntegrationConfigs, + AzureIntegrationConfig, +} from '@backstage/integration'; export class AzurePreparer implements PreparerBase { - private readonly privateToken: string; + private readonly integrations: AzureIntegrationConfig[]; + private readonly scaffolderToken: string | undefined; + private readonly logger: Logger; - constructor(config: Config) { - this.privateToken = - config.getOptionalString('scaffolder.azure.api.token') ?? ''; + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; + this.integrations = readAzureIntegrationConfigs( + config.getOptionalConfigArray('integrations.azure') ?? [], + ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.scaffolderToken = config.getOptionalString( + 'scaffolder.azure.api.token', + ); + + if (this.scaffolderToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); - const { logger } = opts; if (!['azure/api', 'url'].includes(protocol)) { throw new InputError( @@ -57,15 +81,17 @@ export class AzurePreparer implements PreparerBase { template.spec.path ?? '.', ); + const token = this.getToken(parsedGitLocation.resource); + // Username can be anything but the empty string according to: // https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat - const git = this.privateToken + const git = token ? Git.fromAuth({ - password: this.privateToken, + password: token, username: 'notempty', - logger, + logger: this.logger, }) - : Git.fromAuth({ logger }); + : Git.fromAuth({ logger: this.logger }); await git.clone({ url: repositoryCheckoutUrl, @@ -74,4 +100,11 @@ export class AzurePreparer implements PreparerBase { return path.resolve(tempDir, templateDirectory); } + + private getToken(host: string): string | undefined { + return ( + this.scaffolderToken || + this.integrations.find(c => c.host === host)?.token + ); + } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index aa040bfe58..2b3e9698b2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -26,12 +26,14 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { getVoidLogger, Git } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; describe('GitHubPreparer', () => { let mockEntity: TemplateEntityV1alpha1; const mockGitClient = { clone: jest.fn(), }; + const logger = getVoidLogger(); jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); @@ -77,9 +79,18 @@ describe('GitHubPreparer', () => { }; }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new GithubPreparer(); + const preparer = new GithubPreparer( + new ConfigReader({ + scaffolder: { + github: { + token: 'fake-token', + }, + }, + }), + { logger }, + ); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', @@ -123,15 +134,42 @@ describe('GitHubPreparer', () => { ); }); - it('calls the clone command with the token when provided', async () => { - const preparer = new GithubPreparer({ token: 'abc' }); - const logger = getVoidLogger(); + it('calls the clone command with deprecated token', async () => { + const preparer = new GithubPreparer( + new ConfigReader({ + scaffolder: { + github: { + token: 'fake-token', + }, + }, + }), + { logger }, + ); - await preparer.prepare(mockEntity, { logger }); + await preparer.prepare(mockEntity); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, - username: 'abc', + username: 'fake-token', + password: 'x-oauth-basic', + }); + }); + + it('calls the clone command with token from integrations config', async () => { + const preparer = new GithubPreparer( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'fake-me' }], + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'fake-me', password: 'x-oauth-basic', }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index e7fa0c9b72..46080c85a7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -20,22 +20,46 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; +import { Logger } from 'winston'; import GitUriParser from 'git-url-parse'; +import { Config } from '@backstage/config'; +import { + GitHubIntegrationConfig, + readGitHubIntegrationConfigs, +} from '@backstage/integration'; export class GithubPreparer implements PreparerBase { - token?: string; + private readonly integrations: GitHubIntegrationConfig[]; + private readonly scaffolderToken: string | undefined; + private readonly logger: Logger; - constructor(params: { token?: string } = {}) { - this.token = params.token; + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; + this.integrations = readGitHubIntegrationConfigs( + config.getOptionalConfigArray('integrations.github') ?? [], + ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for Github in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); + + if (this.scaffolderToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); - const { logger } = opts; if (!['github', 'url'].includes(protocol)) { throw new InputError( @@ -57,13 +81,15 @@ export class GithubPreparer implements PreparerBase { const checkoutLocation = path.resolve(tempDir, templateDirectory); - const git = this.token + const token = this.getToken(parsedGitLocation.resource); + + const git = token ? Git.fromAuth({ - username: this.token, + username: token, password: 'x-oauth-basic', - logger, + logger: this.logger, }) - : Git.fromAuth({ logger }); + : Git.fromAuth({ logger: this.logger }); await git.clone({ url: repositoryCheckoutUrl, @@ -72,4 +98,10 @@ export class GithubPreparer implements PreparerBase { return checkoutLocation; } + private getToken(host: string): string | undefined { + return ( + this.scaffolderToken || + this.integrations.find(c => c.host === host)?.token + ); + } } 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 8dad349a13..8cb54ceb7a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -70,6 +70,7 @@ describe('GitLabPreparer', () => { const mockGitClient = { clone: jest.fn(), }; + const logger = getVoidLogger(); jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); @@ -79,10 +80,10 @@ describe('GitLabPreparer', () => { ['gitlab', 'gitlab/api'].forEach(protocol => { it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); + const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -102,11 +103,11 @@ describe('GitLabPreparer', () => { ], }, }), + { logger }, ); mockEntity = mockEntityWithProtocol(protocol); - const logger = getVoidLogger(); - await preparer.prepare(mockEntity, { logger }); + await preparer.prepare(mockEntity); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -122,11 +123,11 @@ describe('GitLabPreparer', () => { gitlab: { api: { token: 'fake-token' } }, }, }), + { logger }, ); mockEntity = mockEntityWithProtocol(protocol); - const logger = getVoidLogger(); - await preparer.prepare(mockEntity, { logger }); + await preparer.prepare(mockEntity); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -136,11 +137,11 @@ describe('GitLabPreparer', () => { }); it(`calls the clone command with the correct arguments for a repository when no path is provided using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); + const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); delete mockEntity.spec.path; - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -149,23 +150,19 @@ describe('GitLabPreparer', () => { }); it(`return the temp directory with the path to the folder if it is specified using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); + const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); - + const response = await preparer.prepare(mockEntity); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); + const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), workingDirectory: '/workDir', }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 6e169baa98..d0ce4ff3f7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -26,26 +26,41 @@ import os from 'os'; import path from 'path'; import { parseLocationAnnotation } from '../helpers'; import { PreparerBase, PreparerOptions } from './types'; +import { Logger } from 'winston'; export class GitlabPreparer implements PreparerBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; + private readonly logger: Logger; - constructor(config: Config) { + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; this.integrations = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + this.scaffolderToken = config.getOptionalString( 'scaffolder.gitlab.api.token', ); + + if (this.scaffolderToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const { logger } = opts; const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); if (!['gitlab', 'gitlab/api', 'url'].includes(protocol)) { @@ -71,9 +86,9 @@ export class GitlabPreparer implements PreparerBase { ? Git.fromAuth({ password: token, username: 'oauth2', - logger, + logger: this.logger, }) - : Git.fromAuth({ logger }); + : Git.fromAuth({ logger: this.logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index 84be130b79..c91711f85a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -77,31 +77,15 @@ export class Preparers implements PreparerBuilder { const preparers = new Preparers(typeDetector); const filePreparer = new FilePreparer(); - const gitlabPreparer = new GitlabPreparer(config); - const azurePreparer = new AzurePreparer(config); + const gitlabPreparer = new GitlabPreparer(config, { logger }); + const azurePreparer = new AzurePreparer(config, { logger }); + const githubPreparer = new GithubPreparer(config, { logger }); preparers.register('file', filePreparer); preparers.register('gitlab', gitlabPreparer); preparers.register('gitlab/api', gitlabPreparer); preparers.register('azure/api', azurePreparer); - - const githubConfig = config.getOptionalConfig('scaffolder.github'); - if (githubConfig) { - try { - const githubToken = githubConfig.getString('token'); - const githubPreparer = new GithubPreparer({ token: githubToken }); - - preparers.register('github', githubPreparer); - } catch (e) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize github scaffolding provider, ${e.message}`, - ); - } - - logger.warn(`Skipping github scaffolding provider, ${e.message}`); - } - } + preparers.register('github', githubPreparer); return preparers; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts index 4936461c5c..34e0fe3696 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts @@ -14,15 +14,13 @@ * limitations under the License. */ import type { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { Logger } from 'winston'; import { RemoteProtocol } from '../types'; export type PreparerOptions = { - logger: Logger; workingDirectory?: string; }; -export type PreparerBase = { +export interface PreparerBase { /** * Given an Entity definition from the Service Catalog, go and prepare a directory * with contents from the remote location in temporary storage and return the path @@ -30,9 +28,9 @@ export type PreparerBase = { */ prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise; -}; +} export type PreparerBuilder = { register(protocol: RemoteProtocol, preparer: PreparerBase): void; From 19741d2e67a516e8c38eec90900c7b3aed21deb9 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 2 Jan 2021 16:45:21 +0100 Subject: [PATCH 02/44] feat(scaffolder-backend/preparers): finished the deprecation of the scaffolder config object for preparers --- .../src/scaffolder/stages/prepare/github.test.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 2b3e9698b2..6598a8734a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -98,10 +98,10 @@ describe('GitHubPreparer', () => { }); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new GithubPreparer(); + const preparer = new GithubPreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', @@ -110,22 +110,18 @@ describe('GitHubPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(); + const preparer = new GithubPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); - + const response = await preparer.prepare(mockEntity); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(); + const preparer = new GithubPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), workingDirectory: '/workDir', }); From c5cdc7e74d79421e6b11f0a7dd1583099414b752 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 4 Jan 2021 10:56:46 +0100 Subject: [PATCH 03/44] chore: starting the migration of a publisher --- .../src/scaffolder/stages/publish/azure.ts | 4 +- .../src/scaffolder/stages/publish/gitlab.ts | 37 +++++++++++++++---- .../src/scaffolder/stages/publish/types.ts | 1 - 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index ffe6845f01..8947c09e60 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -17,7 +17,7 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { GitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { JsonValue } from '@backstage/config'; +import { JsonValue, Config } from '@backstage/config'; import { RequiredTemplateValues } from '../templater'; import { initRepoAndPush } from './helpers'; @@ -25,7 +25,7 @@ export class AzurePublisher implements PublisherBase { private readonly client: GitApi; private readonly token: string; - constructor(client: GitApi, token: string) { + constructor(config: Config) { this.client = client; this.token = token; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index 156a4a249c..66d110d2d7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -16,23 +16,46 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { Gitlab } from '@gitbeaker/core'; -import { JsonValue } from '@backstage/config'; +import { Config, JsonValue } from '@backstage/config'; +import { Logger } from 'winston'; import { initRepoAndPush } from './helpers'; import { RequiredTemplateValues } from '../templater'; +import { + GitLabIntegrationConfig, + readGitLabIntegrationConfigs, +} from '@backstage/integration'; export class GitlabPublisher implements PublisherBase { - private readonly client: Gitlab; - private readonly token: string; + private readonly integrations: GitLabIntegrationConfig[]; + private readonly scaffolderToken: string | undefined; + private readonly logger: Logger; - constructor(client: Gitlab, token: string) { - this.client = client; - this.token = token; + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; + this.integrations = readGitLabIntegrationConfigs( + config.getOptionalConfigArray('integrations.gitlab') ?? [], + ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.scaffolderToken = config.getOptionalString( + 'scaffolder.gitlab.api.token', + ); + + if (this.scaffolderToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ values, directory, - logger, }: PublisherOptions): Promise { const remoteUrl = await this.createRemote(values); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index bc6b63cc57..807d7c82cf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -35,7 +35,6 @@ export type PublisherBase = { export type PublisherOptions = { values: RequiredTemplateValues & Record; - logger: Logger; directory: string; }; From ed4eaa79351ba47276a8fe13b202210b5732ace2 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 7 Jan 2021 02:34:22 +0100 Subject: [PATCH 04/44] chore: some work to more bb to integrations --- .../src/scaffolder/stages/prepare/bitbucket.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 5b244d24ce..8ee39f41fa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -20,14 +20,21 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; +import { readBitbucketIntegrationConfigs, BitbucketIntegrationConfig } from '@backstage/integration'; import GitUriParser from 'git-url-parse'; import { Config } from '@backstage/config'; +import { Logger } from 'winston'; export class BitbucketPreparer implements PreparerBase { private readonly privateToken: string; private readonly username: string; - - constructor(config: Config) { + private readonly logger: Logger; + private readonly integrations: BitbucketIntegrationConfig[]; + constructor(config: Config, { logger }: { logger: Logger}) { + this.logger = logger; + this.integrations = readBitbucketIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucket') ??Takj + ) this.username = config.getOptionalString('scaffolder.bitbucket.api.username') ?? ''; this.privateToken = From 6547e5aa94e2ef44c96fcd1f492b746694b77dcd Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 8 Jan 2021 15:43:38 +0100 Subject: [PATCH 05/44] chore: updating some more things --- .../scaffolder/stages/prepare/bitbucket.ts | 26 +++++++++++++++---- .../src/scaffolder/stages/publish/helpers.ts | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 8ee39f41fa..5bc58e34f0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -20,7 +20,10 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; -import { readBitbucketIntegrationConfigs, BitbucketIntegrationConfig } from '@backstage/integration'; +import { + readBitbucketIntegrationConfigs, + BitbucketIntegrationConfig, +} from '@backstage/integration'; import GitUriParser from 'git-url-parse'; import { Config } from '@backstage/config'; import { Logger } from 'winston'; @@ -29,16 +32,29 @@ export class BitbucketPreparer implements PreparerBase { private readonly privateToken: string; private readonly username: string; private readonly logger: Logger; - private readonly integrations: BitbucketIntegrationConfig[]; - constructor(config: Config, { logger }: { logger: Logger}) { + private readonly integrations: BitbucketIntegrationConfig[]; + constructor(config: Config, { logger }: { logger: Logger }) { this.logger = logger; this.integrations = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ??Takj - ) + config.getOptionalConfigArray('integrations.bitbucket') ?? [], + ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for BitBucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + this.username = config.getOptionalString('scaffolder.bitbucket.api.username') ?? ''; this.privateToken = config.getOptionalString('scaffolder.bitbucket.api.token') ?? ''; + + if (this.username || this.privateToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async prepare( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts index 796e48b8f8..ffefb88cf0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts @@ -39,7 +39,7 @@ export async function initRepoAndPush({ dir, }); - const paths = await globby(['./**', './**/.*'], { + const paths = await globby(['./**', './**/.*', '!.git'], { cwd: dir, gitignore: true, dot: true, From 83719211eb772ad7551487d838a7c7a1e6e1d9d2 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 9 Jan 2021 22:44:29 +0100 Subject: [PATCH 06/44] chore(scaffolder/bitbucket): updating bitbucket preparer to use integrations config --- .../scaffolder/stages/prepare/bitbucket.ts | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 5bc58e34f0..b992861c57 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -63,7 +63,6 @@ export class BitbucketPreparer implements PreparerBase { ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); - const { logger } = opts; if (!['bitbucket', 'url'].includes(protocol)) { throw new InputError( @@ -86,13 +85,14 @@ export class BitbucketPreparer implements PreparerBase { const checkoutLocation = path.resolve(tempDir, templateDirectory); - const git = this.privateToken + const auth = this.getAuth(repo.resource); + + const git = auth ? Git.fromAuth({ - username: this.username, - password: this.privateToken, - logger, + ...auth, + logger: this.logger, }) - : Git.fromAuth({ logger }); + : Git.fromAuth({ logger: this.logger }); await git.clone({ url: repositoryCheckoutUrl, @@ -101,4 +101,32 @@ export class BitbucketPreparer implements PreparerBase { return checkoutLocation; } + + private getAuth( + host: string, + ): { username: string; password: string } | undefined { + if (this.username && this.privateToken) { + return { username: this.username, password: this.privateToken }; + } + + const bitbucketIntegrationConfig = this.integrations.find( + c => c.host === host, + ); + + // TODO(blam): Not sure how appPassword fits in here. Just doing the most simple of + // implementations with the intergations config for now but can maybe fallback to + // appPassword instead maybe at a later stage. + if ( + !bitbucketIntegrationConfig || + !bitbucketIntegrationConfig.username || + !bitbucketIntegrationConfig.token + ) { + return undefined; + } + + return { + username: bitbucketIntegrationConfig.username, + password: bitbucketIntegrationConfig.token, + }; + } } From 4330a51457bf4f12cc5e0a40756ac076eec008d6 Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 10 Jan 2021 00:59:44 +0100 Subject: [PATCH 07/44] feat: porting bitbucket preparer to use integrations cofnig --- .../stages/prepare/bitbucket.test.ts | 104 +++++++++++++++--- .../scaffolder/stages/prepare/bitbucket.ts | 19 ++-- 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index 809d18c808..0be61a9f82 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -30,6 +30,7 @@ import { ConfigReader } from '@backstage/config'; describe('BitbucketPreparer', () => { let mockEntity: TemplateEntityV1alpha1; + const logger = getVoidLogger(); const mockGitClient = { clone: jest.fn(), }; @@ -79,8 +80,8 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), @@ -100,18 +101,22 @@ describe('BitbucketPreparer', () => { ], }, }), + { logger }, ); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://bitbucket.org/backstage-project/backstage-repo', - dir: expect.any(String), + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'fake-user', + password: 'fake-password', }); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); + const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), @@ -119,22 +124,93 @@ describe('BitbucketPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); + const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); + const response = await preparer.prepare(mockEntity); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); }); + it('calls the clone command with deprecated auth method', async () => { + const preparer = new BitbucketPreparer( + new ConfigReader({ + scaffolder: { + bitbucket: { + api: { + username: 'fakeusername', + token: 'faketoken', + }, + }, + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'fakeusername', + password: 'faketoken', + }); + }); + + it('calls the clone command with integrations config for auth method', async () => { + const preparer = new BitbucketPreparer( + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + username: 'asd3', + token: 'faketoken', + }, + ], + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'asd3', + password: 'faketoken', + }); + }); + + it('calls the clone command with integrations config with appPassword for auth method', async () => { + const preparer = new BitbucketPreparer( + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + username: 'asd3', + appPassword: 'myapppassword', + }, + ], + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'asd3', + password: 'myapppassword', + }); + }); + it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); + const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), workingDirectory: '/workDir', }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index b992861c57..cc647982d8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -59,7 +59,7 @@ export class BitbucketPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); @@ -113,20 +113,25 @@ export class BitbucketPreparer implements PreparerBase { c => c.host === host, ); - // TODO(blam): Not sure how appPassword fits in here. Just doing the most simple of - // implementations with the intergations config for now but can maybe fallback to - // appPassword instead maybe at a later stage. + if (!bitbucketIntegrationConfig) { + return undefined; + } + if ( - !bitbucketIntegrationConfig || !bitbucketIntegrationConfig.username || - !bitbucketIntegrationConfig.token + !( + bitbucketIntegrationConfig.token || + bitbucketIntegrationConfig.appPassword + ) ) { return undefined; } return { username: bitbucketIntegrationConfig.username, - password: bitbucketIntegrationConfig.token, + password: + bitbucketIntegrationConfig.token! || + bitbucketIntegrationConfig.appPassword!, }; } } From d5fa852aa2f8dac3f6464c41ca849978f9198963 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 11 Jan 2021 15:52:14 +0100 Subject: [PATCH 08/44] chore: more work to including the integrations config everywhere --- .../scaffolder/stages/prepare/bitbucket.ts | 2 +- .../src/scaffolder/stages/publish/azure.ts | 7 ++- .../src/scaffolder/stages/publish/gitlab.ts | 57 +++++++++++++++++-- .../components/TemplatePage/TemplatePage.tsx | 4 +- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index cc647982d8..8a9e4f452d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -52,7 +52,7 @@ export class BitbucketPreparer implements PreparerBase { if (this.username || this.privateToken) { this.logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", + "DEPRECATION: Using the token format under 'scaffolder.bitbucket.token' will not be respected in future releases. Please consider using integrations config instead", ); } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index 8947c09e60..fe4637b971 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -20,12 +20,15 @@ import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/Git import { JsonValue, Config } from '@backstage/config'; import { RequiredTemplateValues } from '../templater'; import { initRepoAndPush } from './helpers'; - +import { Config } from '@backstage/config'; +import { Logger } from 'winston'; export class AzurePublisher implements PublisherBase { private readonly client: GitApi; private readonly token: string; + private readonly logger: Logger; - constructor(config: Config) { + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; this.client = client; this.token = token; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index 4fb47813b2..cda9a83019 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -28,6 +28,7 @@ import { export class GitlabPublisher implements PublisherBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; + private readonly apiBaseUrl: string | undefined; private readonly logger: Logger; constructor(config: Config, { logger }: { logger: Logger }) { @@ -46,11 +47,19 @@ export class GitlabPublisher implements PublisherBase { 'scaffolder.gitlab.api.token', ); + this.apiBaseUrl = config.getOptionalString('scaffolder.gitlab.api.baseUrl'); + if (this.scaffolderToken) { this.logger.warn( "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", ); } + + if (this.apiBaseUrl) { + this.logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.gitlab.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ @@ -58,20 +67,47 @@ export class GitlabPublisher implements PublisherBase { directory, }: PublisherOptions): Promise { const remoteUrl = await this.createRemote(values); + const { host } = new URL(remoteUrl); + const token = this.getToken(host); + + if (!token) { + throw new Error('No token provided to create the remote repository'); + } await initRepoAndPush({ dir: directory, remoteUrl, auth: { username: 'oauth2', - password: this.token, + password: token, }, - logger, + logger: this.logger, }); return { remoteUrl }; } + private getToken(host: string): string | undefined { + return ( + this.scaffolderToken || + this.integrations.find(c => c.host === host)?.token + ); + } + + private getBaseUrl(host: string): string | undefined { + return ( + this.apiBaseUrl || + this.integrations.find(c => c.host === host)?.apiBaseUrl + ); + } + + private getConfig(host: string): { baseUrl?: string; token?: string } { + return { + baseUrl: this.getBaseUrl(host), + token: this.getToken(host), + }; + } + private async createRemote( values: RequiredTemplateValues & Record, ) { @@ -80,15 +116,24 @@ export class GitlabPublisher implements PublisherBase { pathElements.pop(); const owner = pathElements.join('/'); - let targetNamespace = ((await this.client.Namespaces.show(owner)) as { + const config = this.getConfig(); + + if (!config.token) { + throw new Error( + 'No authentication set for Gitlab publisher. Creating the remote repository is not possible without a token', + ); + } + + const client = new Gitlab({ host: config.baseUrl, token: config.token }); + + let targetNamespace = ((await client.Namespaces.show(owner)) as { id: number; }).id; if (!targetNamespace) { - targetNamespace = ((await this.client.Users.current()) as { id: number }) - .id; + targetNamespace = ((await client.Users.current()) as { id: number }).id; } - const project = (await this.client.Projects.create({ + const project = (await client.Projects.create({ namespace_id: targetNamespace, name: name, })) as { http_url_to_repo: string }; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index fd4e4c3ded..aceaf673e2 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -63,7 +63,7 @@ const OWNER_REPO_SCHEMA = { description: 'Who is going to own this component', }, storePath: { - format: 'GitHub user or org / Repo name', + format: 'storeLocation', type: 'string' as const, title: 'Store path', description: 'GitHub store path in org/repo format', @@ -77,7 +77,7 @@ const OWNER_REPO_SCHEMA = { }; const REPO_FORMAT = { - 'GitHub user or org / Repo name': /[^\/]*\/[^\/]*/, + storeLocation: /[^\/]*\/[^\/]*/, }; export const TemplatePage = () => { From 5224a49a26849ce71a14b3def3e54012ff677eba Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 14 Jan 2021 13:17:38 +0100 Subject: [PATCH 09/44] chore: parse the URL to get the resource in the gitlab preparer for now --- .../src/scaffolder/stages/publish/gitlab.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index cda9a83019..c592b06807 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -20,6 +20,8 @@ import { Config, JsonValue } from '@backstage/config'; import { Logger } from 'winston'; import { initRepoAndPush } from './helpers'; import { RequiredTemplateValues } from '../templater'; +import gitUrlParse from 'git-url-parse'; + import { GitLabIntegrationConfig, readGitLabIntegrationConfigs, @@ -116,7 +118,9 @@ export class GitlabPublisher implements PublisherBase { pathElements.pop(); const owner = pathElements.join('/'); - const config = this.getConfig(); + const { resource: host } = gitUrlParse(values.storePath); + + const config = this.getConfig(host); if (!config.token) { throw new Error( From 2249b22e3ee965796279376ca562ff45b12f73c5 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 14 Jan 2021 18:51:03 +0100 Subject: [PATCH 10/44] chore: added a validate function for storepath --- plugins/scaffolder/package.json | 1 + .../components/TemplatePage/TemplatePage.tsx | 13 +++++++++++ yarn.lock | 23 +++++++++++-------- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 0d6c1bdb37..658e176c6c 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -40,6 +40,7 @@ "@rjsf/core": "^2.4.0", "@rjsf/material-ui": "^2.4.0", "classnames": "^2.2.6", + "git-url-parse": "^11.4.3", "moment": "^2.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index aceaf673e2..f703c9dea6 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -39,6 +39,7 @@ import { rootRoute } from '../../routes'; import { JobStatusModal } from '../JobStatusModal'; import { MultistepJsonForm } from '../MultistepJsonForm'; import { useJobPolling } from '../hooks/useJobPolling'; +import gitParse from 'git-url-parse'; const useTemplate = ( templateName: string, @@ -183,6 +184,18 @@ export const TemplatePage = () => { label: 'Choose owner and repo', schema: OWNER_REPO_SCHEMA, customFormats: REPO_FORMAT, + validate: (formData, errors) => { + const { storePath } = formData; + const parsedUrl = gitParse(storePath); + + if (!parsedUrl.resource) { + errors.storePath.addError( + 'There needs to be a hostname in the storePath', + ); + } + + return errors; + }, }, ]} /> diff --git a/yarn.lock b/yarn.lock index 557a8e60e5..3f37440667 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1644,9 +1644,11 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.6.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" @@ -1655,9 +1657,11 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.6.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" @@ -1666,16 +1670,17 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.4.3" + 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" From 548dbf154ee42f9dc7d29b985c2067ef11c7cc6c Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 15 Jan 2021 14:34:21 +0100 Subject: [PATCH 11/44] Fix prepare, e2e tested with GitHub --- .../src/scaffolder/stages/prepare/azure.ts | 15 +- .../scaffolder/stages/prepare/bitbucket.ts | 15 +- .../src/scaffolder/stages/prepare/github.ts | 15 +- .../src/scaffolder/stages/prepare/gitlab.ts | 15 +- .../src/scaffolder/stages/prepare/types.ts | 2 + .../src/scaffolder/stages/publish/github.ts | 130 +++++++++++++----- .../scaffolder/stages/publish/publishers.ts | 29 +--- .../src/scaffolder/stages/publish/types.ts | 1 + .../components/TemplatePage/TemplatePage.tsx | 2 +- 9 files changed, 131 insertions(+), 93 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index bb2257dfa7..b2eab7bb85 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -31,16 +31,14 @@ import { export class AzurePreparer implements PreparerBase { private readonly integrations: AzureIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - private readonly logger: Logger; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; this.integrations = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); if (!this.integrations.length) { - this.logger.warn( + logger.warn( 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', ); } @@ -50,7 +48,7 @@ export class AzurePreparer implements PreparerBase { ); if (this.scaffolderToken) { - this.logger.warn( + logger.warn( "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", ); } @@ -58,10 +56,11 @@ export class AzurePreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: PreparerOptions, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); + const workingDirectory = opts.workingDirectory ?? os.tmpdir(); + const logger = opts.logger; if (!['azure/api', 'url'].includes(protocol)) { throw new InputError( @@ -89,9 +88,9 @@ export class AzurePreparer implements PreparerBase { ? Git.fromAuth({ password: token, username: 'notempty', - logger: this.logger, + logger, }) - : Git.fromAuth({ logger: this.logger }); + : Git.fromAuth({ logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 8a9e4f452d..9dcc61d6fe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -31,16 +31,14 @@ import { Logger } from 'winston'; export class BitbucketPreparer implements PreparerBase { private readonly privateToken: string; private readonly username: string; - private readonly logger: Logger; private readonly integrations: BitbucketIntegrationConfig[]; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; this.integrations = readBitbucketIntegrationConfigs( config.getOptionalConfigArray('integrations.bitbucket') ?? [], ); if (!this.integrations.length) { - this.logger.warn( + logger.warn( 'Integrations for BitBucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', ); } @@ -51,7 +49,7 @@ export class BitbucketPreparer implements PreparerBase { config.getOptionalString('scaffolder.bitbucket.api.token') ?? ''; if (this.username || this.privateToken) { - this.logger.warn( + logger.warn( "DEPRECATION: Using the token format under 'scaffolder.bitbucket.token' will not be respected in future releases. Please consider using integrations config instead", ); } @@ -59,10 +57,11 @@ export class BitbucketPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: PreparerOptions, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); + const workingDirectory = opts.workingDirectory ?? os.tmpdir(); + const logger = opts.logger; if (!['bitbucket', 'url'].includes(protocol)) { throw new InputError( @@ -90,9 +89,9 @@ export class BitbucketPreparer implements PreparerBase { const git = auth ? Git.fromAuth({ ...auth, - logger: this.logger, + logger, }) - : Git.fromAuth({ logger: this.logger }); + : Git.fromAuth({ logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 46080c85a7..f7b0fd801f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -31,16 +31,14 @@ import { export class GithubPreparer implements PreparerBase { private readonly integrations: GitHubIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - private readonly logger: Logger; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; this.integrations = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], ); if (!this.integrations.length) { - this.logger.warn( + logger.warn( 'Integrations for Github in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', ); } @@ -48,7 +46,7 @@ export class GithubPreparer implements PreparerBase { this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); if (this.scaffolderToken) { - this.logger.warn( + logger.warn( "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", ); } @@ -56,10 +54,11 @@ export class GithubPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: PreparerOptions, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); + const workingDirectory = opts.workingDirectory ?? os.tmpdir(); + const logger = opts.logger; if (!['github', 'url'].includes(protocol)) { throw new InputError( @@ -87,9 +86,9 @@ export class GithubPreparer implements PreparerBase { ? Git.fromAuth({ username: token, password: 'x-oauth-basic', - logger: this.logger, + logger, }) - : Git.fromAuth({ logger: this.logger }); + : Git.fromAuth({ logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index d0ce4ff3f7..caf45db37f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -31,16 +31,14 @@ import { Logger } from 'winston'; export class GitlabPreparer implements PreparerBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - private readonly logger: Logger; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; this.integrations = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); if (!this.integrations.length) { - this.logger.warn( + logger.warn( 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', ); } @@ -50,7 +48,7 @@ export class GitlabPreparer implements PreparerBase { ); if (this.scaffolderToken) { - this.logger.warn( + logger.warn( "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", ); } @@ -58,10 +56,11 @@ export class GitlabPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: PreparerOptions, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); + const logger = opts.logger; + const workingDirectory = opts.workingDirectory ?? os.tmpdir(); if (!['gitlab', 'gitlab/api', 'url'].includes(protocol)) { throw new InputError( @@ -86,9 +85,9 @@ export class GitlabPreparer implements PreparerBase { ? Git.fromAuth({ password: token, username: 'oauth2', - logger: this.logger, + logger, }) - : Git.fromAuth({ logger: this.logger }); + : Git.fromAuth({ logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts index 34e0fe3696..2974b833a2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts @@ -15,9 +15,11 @@ */ import type { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { RemoteProtocol } from '../types'; +import { Logger } from 'winston'; export type PreparerOptions = { workingDirectory?: string; + logger: Logger; }; export interface PreparerBase { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 39a588882f..902dc72a77 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -15,32 +15,55 @@ */ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { Octokit } from '@octokit/rest'; import { initRepoAndPush } from './helpers'; -import { JsonValue } from '@backstage/config'; -import { RequiredTemplateValues } from '../templater'; +import { Config } from '@backstage/config'; +import { Logger } from 'winston'; +import { + GitHubIntegrationConfig, + readGitHubIntegrationConfigs, +} from '@backstage/integration'; +import gitUrlParse from 'git-url-parse'; +import { Octokit } from '@octokit/rest'; export type RepoVisibilityOptions = 'private' | 'internal' | 'public'; - -interface GithubPublisherParams { - client: Octokit; - token: string; - repoVisibility: RepoVisibilityOptions; -} - export class GithubPublisher implements PublisherBase { - private client: Octokit; - private token: string; - private repoVisibility: RepoVisibilityOptions; + private scaffolderToken: string | undefined; + private readonly integrations: GitHubIntegrationConfig[]; + private readonly apiBaseUrl: string | undefined; + private readonly repoVisibility: RepoVisibilityOptions; - constructor({ - client, - token, - repoVisibility = 'public', - }: GithubPublisherParams) { - this.client = client; - this.token = token; - this.repoVisibility = repoVisibility; + constructor(config: Config, { logger }: { logger: Logger }) { + this.integrations = readGitHubIntegrationConfigs( + config.getOptionalConfigArray('integrations.github') ?? [], + ); + + if (!this.integrations.length) { + logger.warn( + 'Integrations for GitHub in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.scaffolderToken = config.getOptionalString( + 'scaffolder.github.api.token', + ); + + this.apiBaseUrl = config.getOptionalString('scaffolder.github.api.baseUrl'); + + if (this.scaffolderToken) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.github.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + if (this.apiBaseUrl) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.github.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.repoVisibility = (config.getOptionalString( + 'scaffolder.github.visibility', + ) ?? 'public') as RepoVisibilityOptions; } async publish({ @@ -48,13 +71,29 @@ export class GithubPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const remoteUrl = await this.createRemote(values); + const { resource: host, owner, name } = gitUrlParse(values.storePath); + const token = this.getToken(host); + + if (!token) { + throw new Error('No token provided to create the remote repository'); + } + + const description = values.description as string; + const access = values.access as string; + const remoteUrl = await this.createRemote({ + description, + access, + host, + name, + owner, + token, + }); await initRepoAndPush({ dir: directory, remoteUrl, auth: { - username: this.token, + username: token ?? '', password: 'x-oauth-basic', }, logger, @@ -68,24 +107,34 @@ export class GithubPublisher implements PublisherBase { return { remoteUrl, catalogInfoUrl }; } - private async createRemote( - values: RequiredTemplateValues & Record, - ) { - const [owner, name] = values.storePath.split('/'); - const description = values.description as string; + private async createRemote(opts: { + access: string; + name: string; + owner: string; + host: string; + token: string; + description: string; + }) { + const { access, description, host, owner, name, token } = opts; - const user = await this.client.users.getByUsername({ username: owner }); + // create a github client with the config + const githubClient = new Octokit({ + auth: token, + baseUrl: this.getBaseUrl(host), + }); + + const user = await githubClient.users.getByUsername({ username: owner }); const repoCreationPromise = user.data.type === 'Organization' - ? this.client.repos.createInOrg({ + ? githubClient.repos.createInOrg({ name, org: owner, private: this.repoVisibility !== 'public', visibility: this.repoVisibility, description, }) - : this.client.repos.createForAuthenticatedUser({ + : githubClient.repos.createForAuthenticatedUser({ name, private: this.repoVisibility === 'private', description, @@ -93,10 +142,9 @@ export class GithubPublisher implements PublisherBase { const { data } = await repoCreationPromise; - const access = values.access as string; if (access?.startsWith(`${owner}/`)) { const [, team] = access.split('/'); - await this.client.teams.addOrUpdateRepoPermissionsInOrg({ + await githubClient.teams.addOrUpdateRepoPermissionsInOrg({ org: owner, team_slug: team, owner, @@ -105,7 +153,7 @@ export class GithubPublisher implements PublisherBase { }); // no need to add access if it's the person who own's the personal account } else if (access && access !== owner) { - await this.client.repos.addCollaborator({ + await githubClient.repos.addCollaborator({ owner, repo: name, username: access, @@ -115,4 +163,18 @@ export class GithubPublisher implements PublisherBase { return data?.clone_url; } + + private getToken(host: string): string | undefined { + return ( + this.scaffolderToken || + this.integrations.find(c => c.host === host)?.token + ); + } + + private getBaseUrl(host: string): string | undefined { + return ( + this.apiBaseUrl || + this.integrations.find(c => c.host === host)?.apiBaseUrl + ); + } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 75243aa610..15e350c450 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -15,8 +15,6 @@ */ import { Logger } from 'winston'; -import { Octokit } from '@octokit/rest'; -import { Gitlab } from '@gitbeaker/node'; import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; import { Config } from '@backstage/config'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; @@ -27,7 +25,7 @@ import { } from '../helpers'; import { PublisherBase, PublisherBuilder } from './types'; import { RemoteProtocol } from '../types'; -import { GithubPublisher, RepoVisibilityOptions } from './github'; +import { GithubPublisher } from './github'; import { GitlabPublisher } from './gitlab'; import { AzurePublisher } from './azure'; import { BitbucketPublisher } from './bitbucket'; @@ -80,23 +78,7 @@ export class Publishers implements PublisherBuilder { const githubConfig = config.getOptionalConfig('scaffolder.github'); if (githubConfig) { try { - const repoVisibility = githubConfig.getString( - 'visibility', - ) as RepoVisibilityOptions; - - const githubToken = githubConfig.getString('token'); - const githubHost = - githubConfig.getOptionalString('host') ?? 'https://api.github.com'; - const githubClient = new Octokit({ - auth: githubToken, - baseUrl: githubHost, - }); - const githubPublisher = new GithubPublisher({ - client: githubClient, - token: githubToken, - repoVisibility, - }); - + const githubPublisher = new GithubPublisher(config, { logger }); publishers.register('file', githubPublisher); publishers.register('github', githubPublisher); } catch (e) { @@ -116,12 +98,7 @@ export class Publishers implements PublisherBuilder { const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab'); if (gitLabConfig) { try { - const gitLabToken = gitLabConfig.getConfig('api').getString('token'); - const gitLabClient = new Gitlab({ - host: gitLabConfig.getConfig('api').getOptionalString('baseUrl'), - token: gitLabToken, - }); - const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken); + const gitLabPublisher = new GitlabPublisher(config, { logger }); publishers.register('gitlab', gitLabPublisher); publishers.register('gitlab/api', gitLabPublisher); } catch (e) { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index 807d7c82cf..4164a6a602 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -36,6 +36,7 @@ export type PublisherBase = { export type PublisherOptions = { values: RequiredTemplateValues & Record; directory: string; + logger: Logger; }; export type PublisherResult = { diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index f703c9dea6..4fc2e98bb0 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -190,7 +190,7 @@ export const TemplatePage = () => { if (!parsedUrl.resource) { errors.storePath.addError( - 'There needs to be a hostname in the storePath', + 'There needs to be a hostname in the store path', ); } From 8ef84c67a316e451ffa4f45c4f1dd0e5cae5d28c Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 15 Jan 2021 16:07:29 +0100 Subject: [PATCH 12/44] Update storePath error and description Co-authored-by: blam --- .../src/components/TemplatePage/TemplatePage.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 4fc2e98bb0..856ff3d0e2 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -67,7 +67,8 @@ const OWNER_REPO_SCHEMA = { format: 'storeLocation', type: 'string' as const, title: 'Store path', - description: 'GitHub store path in org/repo format', + description: + 'A full URL to the repository that should be created. e.g https://github.com/backstage/new-repo', }, access: { type: 'string' as const, @@ -188,9 +189,13 @@ export const TemplatePage = () => { const { storePath } = formData; const parsedUrl = gitParse(storePath); - if (!parsedUrl.resource) { + if ( + !parsedUrl.resource || + !parsedUrl.owner || + !parsedUrl.name + ) { errors.storePath.addError( - 'There needs to be a hostname in the store path', + 'The store path should be a complete Git URL to the new repository location. For example https://github.com/backstage/new-repo', ); } From fd876c36155a05ce04e0c8fa71526aafa23937df Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 15 Jan 2021 16:08:10 +0100 Subject: [PATCH 13/44] Refactor azure publisher to use integrations Co-authored-by: blam --- .../src/scaffolder/stages/publish/azure.ts | 87 +++++++++++++++---- .../scaffolder/stages/publish/publishers.ts | 9 +- 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index fe4637b971..ac49a9e4c8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -15,22 +15,48 @@ */ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { GitApi } from 'azure-devops-node-api/GitApi'; +import { IGitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { JsonValue, Config } from '@backstage/config'; -import { RequiredTemplateValues } from '../templater'; -import { initRepoAndPush } from './helpers'; import { Config } from '@backstage/config'; +import { initRepoAndPush } from './helpers'; import { Logger } from 'winston'; +import { + AzureIntegrationConfig, + readAzureIntegrationConfigs, +} from '@backstage/integration'; +import gitUrlParse from 'git-url-parse'; +import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; + export class AzurePublisher implements PublisherBase { - private readonly client: GitApi; - private readonly token: string; - private readonly logger: Logger; + private readonly integrations: AzureIntegrationConfig[]; + private readonly apiBaseUrl?: string; + private readonly token?: string; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; - this.client = client; - this.token = token; + this.integrations = readAzureIntegrationConfigs( + config.getOptionalConfigArray('integrations.azure') ?? [], + ); + + if (!this.integrations.length) { + logger.warn( + 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.token = config.getOptionalString('scaffolder.azure.api.token'); + if (this.token) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.github.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.apiBaseUrl = config.getOptionalString('scaffolder.azure.api.baseUrl'); + + if (this.apiBaseUrl) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.azure.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ @@ -38,7 +64,25 @@ export class AzurePublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const remoteUrl = await this.createRemote(values); + const { resource: host, owner, name } = gitUrlParse(values.storePath); + + const token = this.getToken(host); + if (!token) { + throw new Error('No token provided to create the remote repository'); + } + const baseUrl = this.getBaseUrl(host); + if (!baseUrl) { + throw new Error('No baseUrl provided to create the remote repository'); + } + + const authHandler = getPersonalAccessTokenHandler(token); + const webApi = new WebApi(baseUrl, authHandler); + const azureClient = await webApi.getGitApi(); + + const remoteUrl = await this.createRemote(azureClient, { + project: owner, + name, + }); const catalogInfoUrl = `${remoteUrl}?path=%2Fcatalog-info.yaml`; await initRepoAndPush({ @@ -46,7 +90,7 @@ export class AzurePublisher implements PublisherBase { remoteUrl, auth: { username: 'notempty', - password: this.token, + password: token, }, logger, }); @@ -55,13 +99,24 @@ export class AzurePublisher implements PublisherBase { } private async createRemote( - values: RequiredTemplateValues & Record, + client: IGitApi, + opts: { name: string; project: string }, ) { - const [project, name] = values.storePath.split('/'); - + // const [project, name] = values.storePath.split('/'); + const { name, project } = opts; const createOptions: GitRepositoryCreateOptions = { name }; - const repo = await this.client.createRepository(createOptions, project); + const repo = await client.createRepository(createOptions, project); return repo.remoteUrl || ''; } + + private getToken(host: string): string | undefined { + return this.token || this.integrations.find(c => c.host === host)?.token; + } + + private getBaseUrl(host: string): string | undefined { + return ( + this.apiBaseUrl || this.integrations.find(c => c.host === host)?.host + ); + } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 15e350c450..3d6c557714 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -118,14 +118,7 @@ export class Publishers implements PublisherBuilder { const azureConfig = config.getOptionalConfig('scaffolder.azure'); if (azureConfig) { try { - const baseUrl = azureConfig.getString('baseUrl'); - const azureToken = azureConfig.getConfig('api').getString('token'); - - const authHandler = getPersonalAccessTokenHandler(azureToken); - const webApi = new WebApi(baseUrl, authHandler); - const azureClient = await webApi.getGitApi(); - - const azurePublisher = new AzurePublisher(azureClient, azureToken); + const azurePublisher = new AzurePublisher(config, { logger }); publishers.register('azure/api', azurePublisher); } catch (e) { const providerName = 'azure'; From 7c52806a48dcab14bdcbd8143692c0008c1e0374 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 10:11:52 +0100 Subject: [PATCH 14/44] remove commented code --- .../scaffolder-backend/src/scaffolder/stages/publish/azure.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index ac49a9e4c8..50de3331d8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -102,7 +102,6 @@ export class AzurePublisher implements PublisherBase { client: IGitApi, opts: { name: string; project: string }, ) { - // const [project, name] = values.storePath.split('/'); const { name, project } = opts; const createOptions: GitRepositoryCreateOptions = { name }; const repo = await client.createRepository(createOptions, project); From fdecdfd747ab79f598fb57731e4ede375ca28822 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 10:13:07 +0100 Subject: [PATCH 15/44] Use integration config in bitbucket preparer --- .../scaffolder/stages/publish/bitbucket.ts | 135 ++++++++++++++---- 1 file changed, 107 insertions(+), 28 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index ad83ad54e8..b07e77e49a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -16,19 +16,54 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; -import { RequiredTemplateValues } from '../templater'; -import { JsonValue } from '../../../../../../packages/config/src'; import fetch from 'cross-fetch'; +import { Config } from '@backstage/config'; +import { + BitbucketIntegrationConfig, + readBitbucketIntegrationConfigs, +} from '@backstage/integration'; +import { Logger } from 'winston'; +import gitUrlParse from 'git-url-parse'; export class BitbucketPublisher implements PublisherBase { - private readonly host: string; - private readonly username: string; - private readonly token: string; + private readonly host?: string; + private readonly username?: string; + private readonly token?: string; + private readonly integrations: BitbucketIntegrationConfig[]; - constructor(host: string, username: string, token: string) { - this.host = host; - this.username = username; - this.token = token; + constructor(config: Config, { logger }: { logger: Logger }) { + this.integrations = readBitbucketIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucket') ?? [], + ); + + if (!this.integrations.length) { + logger.warn( + 'Integrations for Bitbucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.token = config.getOptionalString('scaffolder.bitbucket.api.token'); + if (this.token) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.bitbucket.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.host = config.getOptionalString('scaffolder.bitbucket.api.host'); + if (this.host) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.host' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.username = config.getOptionalString( + 'scaffolder.bitbucket.api.username', + ); + if (this.username) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.username' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ @@ -36,33 +71,61 @@ export class BitbucketPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const result = await this.createRemote(values); + const { resource: host, owner: project, name } = gitUrlParse( + values.storePath, + ); + + const token = this.getToken(host); + if (!token) { + throw new Error('No token provided to create the remote repository'); + } + const username = this.getUsername(host); + if (!username) { + throw new Error('No username provided to create the remote repository'); + } + const apiUrl = this.getHost(host); + if (!apiUrl) { + throw new Error('No host provided to create the remote repository'); + } + + const description = values.description as string; + const result = await this.createRemote({ + project, + name, + description, + host, + }); await initRepoAndPush({ dir: directory, remoteUrl: result.remoteUrl, auth: { - username: this.username, - password: this.token, + username: username, + password: token, }, logger, }); return result; } - private async createRemote( - values: RequiredTemplateValues & Record, - ): Promise { - if (this.host === 'https://bitbucket.org') { - return this.createBitbucketCloudRepository(values); + private async createRemote(opts: { + project: string; + name: string; + description: string; + host: string; + }): Promise { + if (opts.host === 'https://bitbucket.org') { + return this.createBitbucketCloudRepository(opts); } - return this.createBitbucketServerRepository(values); + return this.createBitbucketServerRepository(opts); } - private async createBitbucketCloudRepository( - values: RequiredTemplateValues & Record, - ): Promise { - const [project, name] = values.storePath.split('/'); + private async createBitbucketCloudRepository(opts: { + project: string; + name: string; + description: string; + }): Promise { + const { project, name, description } = opts; let response: Response; const buffer = Buffer.from(`${this.username}:${this.token}`, 'utf8'); @@ -71,7 +134,7 @@ export class BitbucketPublisher implements PublisherBase { method: 'POST', body: JSON.stringify({ scm: 'git', - description: values.description, + description: description, }), headers: { Authorization: `Basic ${buffer.toString('base64')}`, @@ -102,17 +165,19 @@ export class BitbucketPublisher implements PublisherBase { throw new Error(`Not a valid response code ${await response.text()}`); } - private async createBitbucketServerRepository( - values: RequiredTemplateValues & Record, - ): Promise { - const [project, name] = values.storePath.split('/'); + private async createBitbucketServerRepository(opts: { + project: string; + name: string; + description: string; + }): Promise { + const { project, name, description } = opts; let response: Response; const options: RequestInit = { method: 'POST', body: JSON.stringify({ name: name, - description: values.description, + description: description, }), headers: { Authorization: `Bearer ${this.token}`, @@ -140,4 +205,18 @@ export class BitbucketPublisher implements PublisherBase { } throw new Error(`Not a valid response code ${await response.text()}`); } + + private getToken(host: string): string | undefined { + return this.token || this.integrations.find(c => c.host === host)?.token; + } + + private getUsername(host: string): string | undefined { + return ( + this.username || this.integrations.find(c => c.host === host)?.username + ); + } + + private getHost(host: string): string | undefined { + return this.host || this.integrations.find(c => c.host === host)?.host; + } } From ca957da3e364f997487af48dc427fefe67f58e97 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 10:25:13 +0100 Subject: [PATCH 16/44] Update packages/integration/src/gitlab/config.ts Co-authored-by: Himanshu Mishra --- packages/integration/src/gitlab/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 28687399e1..2d9f4b39ab 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -31,7 +31,7 @@ export type GitLabIntegrationConfig = { /** * The base URL of the API of this provider, e.g. "https://gitlab.com/api/v4", - * with no trailing slash.s + * with no trailing slash. * * May be omitted specifically for GitLab; then it will be deduced. * From 7b038dd16c7e1edd745026e46b8187e1dd2b49d1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 11:37:11 +0100 Subject: [PATCH 17/44] bump gitbeaker --- plugins/scaffolder-backend/package.json | 4 +- yarn.lock | 52 ++++++++++++++++++------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index c2cef2f73e..efbc73ff4b 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -33,8 +33,8 @@ "@backstage/catalog-model": "^0.6.0", "@backstage/config": "^0.1.2", "@backstage/integration": "^0.1.5", - "@gitbeaker/core": "^25.2.0", - "@gitbeaker/node": "^25.2.0", + "@gitbeaker/core": "^28.0.2", + "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", diff --git a/yarn.lock b/yarn.lock index 3f37440667..32ebc261c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1644,11 +1644,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.6.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" @@ -1657,11 +1655,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.6.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" @@ -1670,17 +1666,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.4.3" 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" @@ -2191,6 +2186,16 @@ li "^1.3.0" xcase "^2.0.1" +"@gitbeaker/core@^28.0.2": + version "28.0.2" + resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-28.0.2.tgz#5c2cbe27a20426a96d9511642153ead18ff88877" + integrity sha512-H+0ChYSxLh+EVNzmOguHaJMR7xB9FlVZGi89JQ7BJ2meq0h1TQl+QaGqTyTNM4GYhn7cYrl7ebGoU0yrnYUWSg== + dependencies: + "@gitbeaker/requester-utils" "^28.0.2" + form-data "^3.0.0" + li "^1.3.0" + xcase "^2.0.1" + "@gitbeaker/node@^25.2.0": version "25.2.0" resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-25.2.0.tgz#cc91e83328ec32de0b1a0dac23accd2385734a66" @@ -2201,6 +2206,16 @@ got "^11.7.0" xcase "^2.0.1" +"@gitbeaker/node@^28.0.2": + version "28.0.2" + resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-28.0.2.tgz#ead4eb9e1b4400d3a6fe9769b50e43e84fc99747" + integrity sha512-FCILxXiRep3PUpe/P2+Ak1KdwzIz3azxf1OY8Jb72gKjNNTgCi4ByFCIpHkvbFZlr7kDKTtcPyEN6oA6qnrCdQ== + dependencies: + "@gitbeaker/core" "^28.0.2" + "@gitbeaker/requester-utils" "^28.0.2" + got "^11.7.0" + xcase "^2.0.1" + "@gitbeaker/requester-utils@^25.2.0", "@gitbeaker/requester-utils@^25.6.0": version "25.6.0" resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-25.6.0.tgz#001a432a48460bb5196a02ed71763eb707a1a01e" @@ -2210,6 +2225,15 @@ query-string "^6.13.3" xcase "^2.0.1" +"@gitbeaker/requester-utils@^28.0.2": + version "28.0.2" + resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-28.0.2.tgz#3c1bf97e6b73053ef3c38a2871cbc8e1a2e7501a" + integrity sha512-S851vQi0x9Sg5Tx0BcwzkG6OTIG0X4qYTBaA9kPM2sEKg2OOsd8X0HRhj/TbFGr/YVz+3Ubu7oueeWSyr5jvfQ== + dependencies: + form-data "^3.0.0" + query-string "^6.13.3" + xcase "^2.0.1" + "@google-cloud/common@^3.5.0": version "3.5.0" resolved "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz#0959e769e8075a06eb0823cc567eef00fd0c2d02" From 931f0c6b89a861e1f37b07cbd9df75a72a79672d Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 11:37:50 +0100 Subject: [PATCH 18/44] Publish to provider based on url --- .../src/scaffolder/jobs/processor.ts | 1 + .../src/scaffolder/stages/publish/gitlab.ts | 71 ++++++++++--------- .../scaffolder/stages/publish/publishers.ts | 56 ++++++--------- .../src/scaffolder/stages/publish/types.ts | 3 +- .../scaffolder-backend/src/service/router.ts | 4 +- 5 files changed, 62 insertions(+), 73 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts index 25f9b23e59..fc11545e04 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts @@ -120,6 +120,7 @@ export class JobProcessor implements Processor { // Log to the current stage the error that occurred and fail the stage. stage.status = 'FAILED'; logger.error(`Stage failed with error: ${error.message}`); + logger.error(error.stack); // Throw the error so the job can be failed too. throw error; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index c592b06807..71a38f1079 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -15,11 +15,10 @@ */ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { Gitlab } from '@gitbeaker/core'; -import { Config, JsonValue } from '@backstage/config'; +import { Gitlab } from '@gitbeaker/node'; +import { Config } from '@backstage/config'; import { Logger } from 'winston'; import { initRepoAndPush } from './helpers'; -import { RequiredTemplateValues } from '../templater'; import gitUrlParse from 'git-url-parse'; import { @@ -67,15 +66,31 @@ export class GitlabPublisher implements PublisherBase { async publish({ values, directory, + logger, }: PublisherOptions): Promise { - const remoteUrl = await this.createRemote(values); - const { host } = new URL(remoteUrl); - const token = this.getToken(host); + const { resource: host, owner, name } = gitUrlParse(values.storePath); + const token = this.getToken(host); if (!token) { - throw new Error('No token provided to create the remote repository'); + throw new Error( + 'No authentication set for Gitlab publisher. Creating the remote repository is not possible without a token', + ); } + const baseUrl = this.getBaseUrl(host); + if (!baseUrl) { + throw new Error( + 'No host set for Gitlab publisher. Creating the remote repository is not possible without a host', + ); + } + + const remoteUrl = await this.createRemote({ + host: baseUrl, + owner, + name, + token, + }); + await initRepoAndPush({ dir: directory, remoteUrl, @@ -83,10 +98,14 @@ export class GitlabPublisher implements PublisherBase { username: 'oauth2', password: token, }, - logger: this.logger, + logger, }); - return { remoteUrl }; + const catalogInfoUrl = remoteUrl.replace( + /\.git$/, + '/-/blob/master/catalog-info.yaml', + ); + return { remoteUrl, catalogInfoUrl }; } private getToken(host: string): string | undefined { @@ -103,33 +122,15 @@ export class GitlabPublisher implements PublisherBase { ); } - private getConfig(host: string): { baseUrl?: string; token?: string } { - return { - baseUrl: this.getBaseUrl(host), - token: this.getToken(host), - }; - } - - private async createRemote( - values: RequiredTemplateValues & Record, - ) { - const pathElements = values.storePath.split('/'); - const name = pathElements[pathElements.length - 1]; - pathElements.pop(); - const owner = pathElements.join('/'); - - const { resource: host } = gitUrlParse(values.storePath); - - const config = this.getConfig(host); - - if (!config.token) { - throw new Error( - 'No authentication set for Gitlab publisher. Creating the remote repository is not possible without a token', - ); - } - - const client = new Gitlab({ host: config.baseUrl, token: config.token }); + private async createRemote(opts: { + host: string; + name: string; + owner: string; + token: string; + }) { + const { owner, name, host, token } = opts; + const client = new Gitlab({ host: host, token: token }); let targetNamespace = ((await client.Namespaces.show(owner)) as { id: number; }).id; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 3d6c557714..78cc85a6c4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -15,13 +15,10 @@ */ import { Logger } from 'winston'; -import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; import { Config } from '@backstage/config'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { DeprecatedLocationTypeDetector, makeDeprecatedLocationTypeDetector, - parseLocationAnnotation, } from '../helpers'; import { PublisherBase, PublisherBuilder } from './types'; import { RemoteProtocol } from '../types'; @@ -39,32 +36,29 @@ export class Publishers implements PublisherBuilder { this.publisherMap.set(protocol, publisher); } - get(template: TemplateEntityV1alpha1): PublisherBase { - const { protocol, location } = parseLocationAnnotation(template); - const publisher = this.publisherMap.get(protocol); + get(storePath: string, { logger }: { logger: Logger }): PublisherBase { + const protocol = this.typeDetector?.(storePath); - if (!publisher) { - if ((protocol as string) === 'url') { - const type = this.typeDetector?.(location); - const detected = type && this.publisherMap.get(type as RemoteProtocol); - if (detected) { - return detected; - } - if (type) { - throw new Error( - `No publisher configuration available for type '${type}' with url "${location}". ` + - "Make sure you've added appropriate configuration in the 'scaffolder' configuration section", - ); - } else { - throw new Error( - `Failed to detect publisher type. Unable to determine integration type for location "${location}". ` + - "Please add appropriate configuration to the 'integrations' configuration section", - ); - } - } - throw new Error(`No publisher registered for type: "${protocol}"`); + if (!protocol) { + throw new Error( + `No matching publisher detected for "${storePath}". Please make sure this host is registered in the integration config`, + ); } + logger.info( + `Selected publisher ${protocol} for publishing to URL ${storePath}`, + ); + + const publisher = this.publisherMap.get(protocol as RemoteProtocol); + if (!publisher) { + throw new Error( + `Failed to detect publisher type. Unable to determine integration type for location "${location}". ` + + "Please add appropriate configuration to the 'integrations' configuration section", + ); + } + + logger.info(`Selected publisher for protocol ${protocol}`); + return publisher; } @@ -139,15 +133,7 @@ export class Publishers implements PublisherBuilder { ); if (bitbucketConfig) { try { - const baseUrl = bitbucketConfig.getString('host'); - const bitbucketUsername = bitbucketConfig.getString('username'); - const bitbucketToken = bitbucketConfig.getString('token'); - - const bitbucketPublisher = new BitbucketPublisher( - baseUrl, - bitbucketUsername, - bitbucketToken, - ); + const bitbucketPublisher = new BitbucketPublisher(config, { logger }); publishers.register('bitbucket', bitbucketPublisher); } catch (e) { const providerName = 'bitbucket'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index 4164a6a602..cb726dfe09 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { RequiredTemplateValues } from '../templater'; import { JsonValue } from '@backstage/config'; import { RemoteProtocol } from '../types'; @@ -46,5 +45,5 @@ export type PublisherResult = { export type PublisherBuilder = { register(protocol: RemoteProtocol, publisher: PublisherBase): void; - get(template: TemplateEntityV1alpha1): PublisherBase; + get(storePath: string, { logger }: { logger: Logger }): PublisherBase; }; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index ffd4d6487c..1ca82d4736 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -154,7 +154,9 @@ export async function createRouter( { name: 'Publish template', handler: async (ctx: StageContext<{ resultDir: string }>) => { - const publisher = publishers.get(ctx.entity); + const publisher = publishers.get(ctx.values.storePath, { + logger: ctx.logger, + }); ctx.logger.info('Will now store the template'); const result = await publisher.publish({ values: ctx.values, From 02e0db404e253102e2e841c88a8a05ce6e4427e5 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 16:39:03 +0100 Subject: [PATCH 19/44] Make azure validation message more helpful Co-authored-by: blam --- .../components/TemplatePage/TemplatePage.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 856ff3d0e2..11c1029c40 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -64,7 +64,6 @@ const OWNER_REPO_SCHEMA = { description: 'Who is going to own this component', }, storePath: { - format: 'storeLocation', type: 'string' as const, title: 'Store path', description: @@ -77,11 +76,6 @@ const OWNER_REPO_SCHEMA = { }, }, }; - -const REPO_FORMAT = { - storeLocation: /[^\/]*\/[^\/]*/, -}; - export const TemplatePage = () => { const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); @@ -184,7 +178,6 @@ export const TemplatePage = () => { { label: 'Choose owner and repo', schema: OWNER_REPO_SCHEMA, - customFormats: REPO_FORMAT, validate: (formData, errors) => { const { storePath } = formData; const parsedUrl = gitParse(storePath); @@ -194,9 +187,15 @@ export const TemplatePage = () => { !parsedUrl.owner || !parsedUrl.name ) { - errors.storePath.addError( - 'The store path should be a complete Git URL to the new repository location. For example https://github.com/backstage/new-repo', - ); + if (parsedUrl.resource === 'dev.azure.com') { + errors.storePath.addError( + "The store path should be formatted like https://dev.azure.com/{org}/{project}/_git/{repo} for Azure URL's", + ); + } else { + errors.storePath.addError( + 'The store path should be a complete Git URL to the new repository location. For example: https://github.com/{owner}/{repo}', + ); + } } return errors; From a33996edcc1cea29337c6d64473d4fdd471219f1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 16:41:01 +0100 Subject: [PATCH 20/44] Scaffolder: Refactor publish and prepare Co-authored-by: blam --- .../src/scaffolder/stages/helpers.test.ts | 11 +- .../src/scaffolder/stages/helpers.ts | 5 +- .../scaffolder/stages/prepare/azure.test.ts | 15 +- .../src/scaffolder/stages/prepare/azure.ts | 2 +- .../stages/prepare/bitbucket.test.ts | 17 +- .../scaffolder/stages/prepare/github.test.ts | 13 +- .../scaffolder/stages/prepare/gitlab.test.ts | 15 +- .../scaffolder/stages/prepare/preparers.ts | 3 +- .../scaffolder/stages/publish/azure.test.ts | 102 +++++++++-- .../src/scaffolder/stages/publish/azure.ts | 1 + .../stages/publish/bitbucket.test.ts | 41 ++++- .../scaffolder/stages/publish/bitbucket.ts | 78 ++++++-- .../scaffolder/stages/publish/github.test.ts | 84 ++++++--- .../scaffolder/stages/publish/gitlab.test.ts | 73 ++++++-- .../stages/publish/publishers.test.ts | 167 ++++++++---------- .../scaffolder/stages/publish/publishers.ts | 86 ++------- .../src/scaffolder/stages/types.ts | 3 +- 17 files changed, 425 insertions(+), 291 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts index 09bd6b7885..d02a0a5ee6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts @@ -30,10 +30,7 @@ describe('Helpers', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'Template', metadata: { - annotations: { - // [LOCATION_ANNOTATION]: - // 'github:https://github.com/benjdlambert/backstage-graphql-template/blob/master/template.yaml', - }, + annotations: {}, name: 'graphql-starter', title: 'GraphQL Service', description: @@ -275,11 +272,11 @@ describe('Helpers', () => { expect(detector('http://derp.org:80/wat')).toBe('gitlab'); expect(detector('https://foo.org/wat')).toBe('gitlab'); expect(detector('http://not.derp.net')).toBe(undefined); - expect(detector('http://derp.net')).toBe('azure/api'); - expect(detector('http://derp.net:8080/wat')).toBe('azure/api'); + expect(detector('http://derp.net')).toBe('azure'); + expect(detector('http://derp.net:8080/wat')).toBe('azure'); expect(detector('http://github.com')).toBe('github'); expect(detector('http://gitlab.com')).toBe('gitlab'); - expect(detector('http://dev.azure.com')).toBe('azure/api'); + expect(detector('http://dev.azure.com')).toBe('azure'); }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts index 21b63609e8..c3ef01b359 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts @@ -74,7 +74,8 @@ export function makeDeprecatedLocationTypeDetector( // These are installed by default by the integrations hostMap.set('github.com', 'github'); hostMap.set('gitlab.com', 'gitlab'); - hostMap.set('dev.azure.com', 'azure/api'); + hostMap.set('dev.azure.com', 'azure'); + hostMap.set('bitbucket.org', 'bitbucket'); config.getOptionalConfigArray('integrations.github')?.forEach(sub => { hostMap.set(sub.getString('host'), 'github'); @@ -83,7 +84,7 @@ export function makeDeprecatedLocationTypeDetector( hostMap.set(sub.getString('host'), 'gitlab'); }); config.getOptionalConfigArray('integrations.azure')?.forEach(sub => { - hostMap.set(sub.getString('host'), 'azure/api'); + hostMap.set(sub.getString('host'), 'azure'); }); config.getOptionalConfigArray('integrations.bitbucket')?.forEach(sub => { hostMap.set(sub.getString('host'), 'bitbucket'); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 38c262eaaa..896d91f4ed 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -46,7 +46,7 @@ describe('AzurePreparer', () => { metadata: { annotations: { [LOCATION_ANNOTATION]: - 'azure/api:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', + 'url:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', }, name: 'graphql-starter', title: 'GraphQL Service', @@ -95,7 +95,7 @@ describe('AzurePreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -116,7 +116,7 @@ describe('AzurePreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -128,7 +128,7 @@ describe('AzurePreparer', () => { it('calls the clone command with the correct arguments for a repository', async () => { const preparer = new AzurePreparer(new ConfigReader({}), { logger }); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: @@ -141,7 +141,7 @@ describe('AzurePreparer', () => { const preparer = new AzurePreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: @@ -154,7 +154,9 @@ describe('AzurePreparer', () => { const preparer = new AzurePreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, @@ -167,6 +169,7 @@ describe('AzurePreparer', () => { const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', + logger: getVoidLogger(), }); expect(response.split('\\').join('/')).toMatch( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index b2eab7bb85..35fa02e8ce 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -62,7 +62,7 @@ export class AzurePreparer implements PreparerBase { const workingDirectory = opts.workingDirectory ?? os.tmpdir(); const logger = opts.logger; - if (!['azure/api', 'url'].includes(protocol)) { + if (!['azure', 'url'].includes(protocol)) { throw new InputError( `Wrong location protocol: ${protocol}, should be 'url'`, ); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index 0be61a9f82..d55ca7a01d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -81,7 +81,7 @@ describe('BitbucketPreparer', () => { it('calls the clone command with the correct arguments for a repository', async () => { const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), @@ -104,7 +104,7 @@ describe('BitbucketPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -116,7 +116,7 @@ describe('BitbucketPreparer', () => { it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), @@ -126,7 +126,9 @@ describe('BitbucketPreparer', () => { it('return the temp directory with the path to the folder if it is specified', async () => { const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, @@ -148,7 +150,7 @@ describe('BitbucketPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -173,7 +175,7 @@ describe('BitbucketPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -198,7 +200,7 @@ describe('BitbucketPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -212,6 +214,7 @@ describe('BitbucketPreparer', () => { mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', + logger: getVoidLogger(), }); expect(response.split('\\').join('/')).toMatch( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 6598a8734a..122a1abb4c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -90,7 +90,7 @@ describe('GitHubPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', @@ -101,7 +101,7 @@ describe('GitHubPreparer', () => { const preparer = new GithubPreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', @@ -112,7 +112,9 @@ describe('GitHubPreparer', () => { it('return the temp directory with the path to the folder if it is specified', async () => { const preparer = new GithubPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); @@ -123,6 +125,7 @@ describe('GitHubPreparer', () => { mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', + logger: getVoidLogger(), }); expect(response.split('\\').join('/')).toMatch( @@ -142,7 +145,7 @@ describe('GitHubPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -161,7 +164,7 @@ describe('GitHubPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, 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 8cb54ceb7a..c1e42920ae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -78,12 +78,12 @@ describe('GitLabPreparer', () => { jest.clearAllMocks(); }); - ['gitlab', 'gitlab/api'].forEach(protocol => { + ['gitlab'].forEach(protocol => { it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -107,7 +107,7 @@ describe('GitLabPreparer', () => { ); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -127,7 +127,7 @@ describe('GitLabPreparer', () => { ); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -141,7 +141,7 @@ describe('GitLabPreparer', () => { mockEntity = mockEntityWithProtocol(protocol); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -153,7 +153,9 @@ describe('GitLabPreparer', () => { const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); @@ -164,6 +166,7 @@ describe('GitLabPreparer', () => { mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', + logger: getVoidLogger(), }); expect(response.split('\\').join('/')).toMatch( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index ff23aa2990..251e8b0ae3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -85,8 +85,7 @@ export class Preparers implements PreparerBuilder { preparers.register('file', filePreparer); preparers.register('gitlab', gitlabPreparer); - preparers.register('gitlab/api', gitlabPreparer); - preparers.register('azure/api', azurePreparer); + preparers.register('azure', azurePreparer); preparers.register('github', githubPreparer); preparers.register('bitbucket', bitbucketPreparer); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts index f1e0cd6175..9aaee4bcb8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts @@ -15,28 +15,45 @@ */ jest.mock('./helpers'); +jest.mock('azure-devops-node-api', () => ({ + WebApi: jest.fn(), + getPersonalAccessTokenHandler: jest.fn(), +})); + import { AzurePublisher } from './azure'; -import { GitApi } from 'azure-devops-node-api/GitApi'; +import { WebApi } from 'azure-devops-node-api'; import * as helpers from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; - -const { mockGitApi } = require('azure-devops-node-api/GitApi') as { - mockGitApi: { - createRepository: jest.MockedFunction; - }; -}; +import { ConfigReader } from '@backstage/config'; describe('Azure Publisher', () => { - const publisher = new AzurePublisher(new GitApi('', []), 'fake-token'); const logger = getVoidLogger(); - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('publish: createRemoteInAzure', () => { it('should use azure-devops-node-api to create a repo in the given project', async () => { - mockGitApi.createRepository.mockResolvedValue({ + const mockGitClient = { + createRepository: jest.fn(), + }; + const mockGitApi = { + getGitApi: jest.fn().mockReturnValue(mockGitClient), + }; + + ((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi); + + const publisher = new AzurePublisher( + new ConfigReader({ + scaffolder: { + azure: { + api: { + baseUrl: 'https://dev.azure.com/myorg', + token: 'fake-azure-token', + }, + }, + }, + }), + { logger }, + ); + mockGitClient.createRepository.mockResolvedValue({ remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', } as { remoteUrl: string }); @@ -54,7 +71,7 @@ describe('Azure Publisher', () => { catalogInfoUrl: 'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml', }); - expect(mockGitApi.createRepository).toHaveBeenCalledWith( + expect(mockGitClient.createRepository).toHaveBeenCalledWith( { name: 'repo', }, @@ -63,7 +80,62 @@ describe('Azure Publisher', () => { expect(helpers.initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - auth: { username: 'notempty', password: 'fake-token' }, + auth: { username: 'notempty', password: 'fake-azure-token' }, + logger, + }); + }); + + it('should use azure-devops-node-api with integrations config', async () => { + const mockGitClient = { + createRepository: jest.fn(), + }; + const mockGitApi = { + getGitApi: jest.fn().mockReturnValue(mockGitClient), + }; + + ((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi); + + const publisher = new AzurePublisher( + new ConfigReader({ + integrations: { + azure: [ + { + host: 'dev.azure.com', + token: 'fake-azure-token', + }, + ], + }, + }), + { logger }, + ); + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + } as { remoteUrl: string }); + + const result = await publisher.publish({ + values: { + storePath: 'https://dev.azure.com/organization/project/_git/repo', + owner: 'bob', + }, + directory: '/tmp/test', + logger, + }); + + expect(result).toEqual({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + catalogInfoUrl: + 'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml', + }); + expect(mockGitClient.createRepository).toHaveBeenCalledWith( + { + name: 'repo', + }, + 'project', + ); + expect(helpers.initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + auth: { username: 'notempty', password: 'fake-azure-token' }, logger, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index 50de3331d8..4172f28f6b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -83,6 +83,7 @@ export class AzurePublisher implements PublisherBase { project: owner, name, }); + const catalogInfoUrl = `${remoteUrl}?path=%2Fcatalog-info.yaml`; await initRepoAndPush({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts index 3b084f1bf9..6cfcc7d69e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts @@ -22,6 +22,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { msw } from '@backstage/test-utils'; +import { ConfigReader } from '@backstage/config'; describe('Bitbucket Publisher', () => { const logger = getVoidLogger(); @@ -59,14 +60,25 @@ describe('Bitbucket Publisher', () => { ); const publisher = new BitbucketPublisher( - 'https://bitbucket.org', - 'fake-user', - 'fake-token', + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + username: 'fake-user', + appPassword: 'fake-token', + }, + ], + }, + }), + { + logger: getVoidLogger(), + }, ); const result = await publisher.publish({ values: { - storePath: 'project/repo', + storePath: 'https://bitbucket.org/project/repo', owner: 'bob', }, directory: '/tmp/test', @@ -87,6 +99,7 @@ describe('Bitbucket Publisher', () => { }); }); }); + describe('publish: createRemoteInBitbucketServer', () => { it('should create repo in bitbucket server', async () => { server.use( @@ -117,14 +130,24 @@ describe('Bitbucket Publisher', () => { ); const publisher = new BitbucketPublisher( - 'https://bitbucket.mycompany.com', - 'fake-user', - 'fake-token', + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.mycompany.com', + token: 'fake-token', + }, + ], + }, + }), + { + logger: getVoidLogger(), + }, ); const result = await publisher.publish({ values: { - storePath: 'project/repo', + storePath: 'https://bitbucket.mycompany.com/project/repo', owner: 'bob', }, directory: '/tmp/test', @@ -140,7 +163,7 @@ describe('Bitbucket Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', - auth: { username: 'fake-user', password: 'fake-token' }, + auth: { username: 'x-token-auth', password: 'fake-token' }, logger: logger, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index b07e77e49a..4cc7654136 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -25,10 +25,14 @@ import { import { Logger } from 'winston'; import gitUrlParse from 'git-url-parse'; +// TODO(blam): We should probably start to use a bitbucket client here that we can change +// the baseURL to point at on-prem or public bitbucket versions like we do for +// github and ghe export class BitbucketPublisher implements PublisherBase { private readonly host?: string; private readonly username?: string; private readonly token?: string; + private readonly appPassword?: string; private readonly integrations: BitbucketIntegrationConfig[]; constructor(config: Config, { logger }: { logger: Logger }) { @@ -64,6 +68,15 @@ export class BitbucketPublisher implements PublisherBase { "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.username' will not be respected in future releases. Please consider using integrations config instead", ); } + + this.appPassword = config.getOptionalString( + 'scaffolder.bitbucket.api.appPassword', + ); + if (this.appPassword) { + logger.warn( + "DEPRECATION: Using the appPassword format under 'scaffolder.bitbucket.api.appassword' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ @@ -71,20 +84,20 @@ export class BitbucketPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const { resource: host, owner: project, name } = gitUrlParse( + const { resource: hostname, owner: project, name } = gitUrlParse( values.storePath, ); - const token = this.getToken(host); - if (!token) { - throw new Error('No token provided to create the remote repository'); + const token = this.getToken(hostname); + const appPassword = this.getAppPassword(hostname); + const username = this.getUsername(hostname); + + if (!username && !appPassword && !token) { + throw new Error('Cannot create repository without bitbucket credentials'); } - const username = this.getUsername(host); - if (!username) { - throw new Error('No username provided to create the remote repository'); - } - const apiUrl = this.getHost(host); - if (!apiUrl) { + const host = this.getHost(hostname); + + if (!host) { throw new Error('No host provided to create the remote repository'); } @@ -94,14 +107,17 @@ export class BitbucketPublisher implements PublisherBase { name, description, host, + username, + token, + appPassword, }); await initRepoAndPush({ dir: directory, remoteUrl: result.remoteUrl, auth: { - username: username, - password: token, + username: username ? username : 'x-token-auth', + password: appPassword ? appPassword : token ?? '', }, logger, }); @@ -109,12 +125,15 @@ export class BitbucketPublisher implements PublisherBase { } private async createRemote(opts: { + username?: string; + token?: string; + appPassword?: string; project: string; name: string; description: string; host: string; }): Promise { - if (opts.host === 'https://bitbucket.org') { + if (opts.host === 'bitbucket.org') { return this.createBitbucketCloudRepository(opts); } return this.createBitbucketServerRepository(opts); @@ -124,11 +143,22 @@ export class BitbucketPublisher implements PublisherBase { project: string; name: string; description: string; + username?: string; + appPassword?: string; }): Promise { - const { project, name, description } = opts; + const { project, name, description, username, appPassword } = opts; + if (!appPassword) { + throw new Error( + 'appPassword is required to create the remote repository', + ); + } + + if (!username) { + throw new Error('username is required to create the remote repository'); + } let response: Response; - const buffer = Buffer.from(`${this.username}:${this.token}`, 'utf8'); + const buffer = Buffer.from(`${username}:${appPassword}`, 'utf8'); const options: RequestInit = { method: 'POST', @@ -169,8 +199,13 @@ export class BitbucketPublisher implements PublisherBase { project: string; name: string; description: string; + token?: string; + host: string; }): Promise { - const { project, name, description } = opts; + const { project, name, description, token, host } = opts; + if (!token) { + throw new Error('No token provided to create the remote repository'); + } let response: Response; const options: RequestInit = { @@ -180,13 +215,13 @@ export class BitbucketPublisher implements PublisherBase { description: description, }), headers: { - Authorization: `Bearer ${this.token}`, + Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }; try { response = await fetch( - `${this.host}/rest/api/1.0/projects/${project}/repos`, + `https://${host}/rest/api/1.0/projects/${project}/repos`, options, ); } catch (e) { @@ -216,6 +251,13 @@ export class BitbucketPublisher implements PublisherBase { ); } + private getAppPassword(host: string): string | undefined { + return ( + this.appPassword || + this.integrations.find(c => c.host === host)?.appPassword + ); + } + private getHost(host: string): string | undefined { return this.host || this.integrations.find(c => c.host === host)?.host; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index 22f1cd3172..effed4763d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -26,6 +26,7 @@ import { import { GithubPublisher } from './github'; import { initRepoAndPush } from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; const { mockGithubClient } = require('@octokit/rest') as { mockGithubClient: { @@ -42,11 +43,21 @@ describe('GitHub Publisher', () => { }); describe('with public repo visibility', () => { - const publisher = new GithubPublisher({ - client: new Octokit(), - token: 'abc', - repoVisibility: 'public', - }); + const publisher = new GithubPublisher( + new ConfigReader({ + integrations: { + github: [ + { + token: 'fake-token', + host: 'github.com', + }, + ], + }, + }), + { + logger, + }, + ); describe('publish: createRemoteInGithub', () => { it('should use octokit to create a repo in an organisation if the organisation property is set', async () => { @@ -63,7 +74,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', access: 'blam/team', }, @@ -94,7 +105,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); @@ -113,7 +124,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', access: 'blam', }, @@ -137,7 +148,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); @@ -157,7 +168,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', access: 'bob', description: 'description', @@ -187,18 +198,26 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); }); describe('with internal repo visibility', () => { - const publisher = new GithubPublisher({ - client: new Octokit(), - token: 'abc', - repoVisibility: 'internal', - }); + const publisher = new GithubPublisher( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'fake-token' }], + }, + scaffolder: { + github: { + visibility: 'internal', + }, + }, + }), + { logger }, + ); it('creates a private repository in the organization with visibility set to internal', async () => { mockGithubClient.repos.createInOrg.mockResolvedValue({ @@ -215,7 +234,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { isOrg: true, - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', }, directory: '/tmp/test', @@ -236,18 +255,33 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); }); describe('private visibility in a user account', () => { - const publisher = new GithubPublisher({ - client: new Octokit(), - token: 'abc', - repoVisibility: 'private', - }); + const publisher = new GithubPublisher( + new ConfigReader({ + integrations: { + github: [ + { + token: 'fake-token', + host: 'github.com', + }, + ], + }, + scaffolder: { + github: { + visibility: 'private', + }, + }, + }), + { + logger, + }, + ); it('creates a private repository', async () => { mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ @@ -263,7 +297,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', }, directory: '/tmp/test', @@ -284,7 +318,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); 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 4894d99291..67887f579f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -14,33 +14,56 @@ * limitations under the License. */ -jest.mock('@gitbeaker/node'); +jest.mock('@gitbeaker/node', () => ({ + Gitlab: jest.fn(), +})); + jest.mock('./helpers'); import { GitlabPublisher } from './gitlab'; -import { Gitlab as GitlabAPI } from '@gitbeaker/core'; import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; - -const { mockGitlabClient } = require('@gitbeaker/node') as { - mockGitlabClient: { - Namespaces: jest.Mocked; - Projects: jest.Mocked; - Users: jest.Mocked; - }; -}; +import { ConfigReader } from '@backstage/config'; describe('GitLab Publisher', () => { const logger = getVoidLogger(); - const publisher = new GitlabPublisher(new Gitlab({}), 'fake-token'); + const mockGitlabClient = { + Namespaces: { + show: jest.fn(), + }, + Projects: { + create: jest.fn(), + }, + Users: { + current: jest.fn(), + }, + }; beforeEach(() => { jest.clearAllMocks(); + + ((Gitlab as unknown) as jest.Mock).mockImplementation( + () => mockGitlabClient, + ); }); describe('publish: createRemoteInGitLab', () => { it('should use gitbeaker to create a repo in a namespace if the namespace property is set', async () => { + const publisher = new GitlabPublisher( + new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'fake-token', + }, + ], + }, + }), + { logger }, + ); + mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 42, } as { id: number }); @@ -51,14 +74,17 @@ describe('GitLab Publisher', () => { const result = await publisher.publish({ values: { isOrg: true, - storePath: 'bloum/blam/test', + storePath: 'https://gitlab.com/blam/test', owner: 'bob', }, directory: '/tmp/test', logger, }); - expect(result).toEqual({ remoteUrl: 'mockclone' }); + expect(result).toEqual({ + remoteUrl: 'mockclone', + catalogInfoUrl: 'mockclone', + }); expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ namespace_id: 42, name: 'test', @@ -72,6 +98,20 @@ describe('GitLab Publisher', () => { }); it('should use gitbeaker to create a repo in the authed user if the namespace property is not set', async () => { + const publisher = new GitlabPublisher( + new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'fake-token', + }, + ], + }, + }), + { logger }, + ); + mockGitlabClient.Namespaces.show.mockResolvedValue({}); mockGitlabClient.Users.current.mockResolvedValue({ id: 21, @@ -82,14 +122,17 @@ describe('GitLab Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'bloum/blam/test', + storePath: 'https://gitlab.com/blam/test', owner: 'bob', }, directory: '/tmp/test', logger, }); - expect(result).toEqual({ remoteUrl: 'mockclone' }); + expect(result).toEqual({ + remoteUrl: 'mockclone', + catalogInfoUrl: 'mockclone', + }); expect(mockGitlabClient.Users.current).toHaveBeenCalled(); expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ namespace_id: 21, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts index b8181b0134..fdbc283f67 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts @@ -14,120 +14,97 @@ * limitations under the License. */ import { Publishers } from './publishers'; -import { - LOCATION_ANNOTATION, - TemplateEntityV1alpha1, -} from '@backstage/catalog-model'; import { GithubPublisher } from './github'; -import { Octokit } from '@octokit/rest'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { AzurePublisher } from './azure'; +import { GitlabPublisher } from './gitlab'; +import { BitbucketPublisher } from './bitbucket'; jest.mock('@octokit/rest'); describe('Publishers', () => { - const mockTemplate: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - [LOCATION_ANNOTATION]: - 'github:https://github.com/benjdlambert/backstage-graphql-template/blob/master/template.yaml', - }, - name: 'graphql-starter', - title: 'GraphQL Service', - description: - 'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n', - uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', - etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: './template', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - }, - }; - it('should throw an error when the publisher for the source location is not registered', () => { const publishers = new Publishers(); - expect(() => publishers.get(mockTemplate)).toThrow( + expect(() => + publishers.get('https://github.com/org/repo', { + logger: getVoidLogger(), + }), + ).toThrow( expect.objectContaining({ - message: 'No publisher registered for type: "github"', + message: + 'No matching publisher detected for "https://github.com/org/repo". Please make sure this host is registered in the integration config', }), ); }); - it('should return the correct preparer when the source matches', () => { - const publishers = new Publishers(); - const publisher = new GithubPublisher({ - client: new Octokit(), - token: 'fake', - repoVisibility: 'public', + it('should return the correct preparer when the source matches for github', async () => { + const publishers = await Publishers.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), }); - publishers.register('github', publisher); - expect(publishers.get(mockTemplate)).toBe(publisher); + expect( + publishers.get('https://github.com/org/repo', { + logger: getVoidLogger(), + }), + ).toBeInstanceOf(GithubPublisher); }); - it('should throw an error if the metadata tag does not exist in the entity', () => { - const brokenTemplate: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: {}, - name: 'react-ssr-template', - title: 'React SSR Template', - description: - 'Next.js application skeleton for creating isomorphic web applications.', - uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: '.', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - }, - }; + it('should return the correct preparer when the source matches for azure', async () => { + const publishers = await Publishers.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), + }); - const publishers = new Publishers(); - - expect(() => publishers.get(brokenTemplate)).toThrow( - expect.objectContaining({ - message: expect.stringContaining('No location annotation provided'), + expect( + publishers.get('https://dev.azure.com/org/project/_git/repo', { + logger: getVoidLogger(), }), + ).toBeInstanceOf(AzurePublisher); + }); + + it('should return the correct preparer when the source matches for bitbucket', async () => { + const publishers = await Publishers.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), + }); + + expect( + publishers.get('https://bitbucket.org/owner/repo', { + logger: getVoidLogger(), + }), + ).toBeInstanceOf(BitbucketPublisher); + }); + + it('should return the correct preparer when the source matches for gitlab', async () => { + const publishers = await Publishers.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), + }); + + expect( + publishers.get('https://gitlab.com/owner/repo', { + logger: getVoidLogger(), + }), + ).toBeInstanceOf(GitlabPublisher); + }); + + it('should respect registrations for custom URLs for providers using the integrations config', async () => { + const publishers = await Publishers.fromConfig( + new ConfigReader({ + integrations: { + github: [ + { host: 'my.special.github.enterprise.thing', token: 'lolghe' }, + ], + }, + }), + { + logger: getVoidLogger(), + }, ); + + expect( + publishers.get('https://my.special.github.enterprise.thing/org/repo', { + logger: getVoidLogger(), + }), + ).toBeInstanceOf(GithubPublisher); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 78cc85a6c4..1fafc33c68 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -52,7 +52,7 @@ export class Publishers implements PublisherBuilder { const publisher = this.publisherMap.get(protocol as RemoteProtocol); if (!publisher) { throw new Error( - `Failed to detect publisher type. Unable to determine integration type for location "${location}". ` + + `Failed to detect publisher type. Unable to determine integration type for location "${protocol}". ` + "Please add appropriate configuration to the 'integrations' configuration section", ); } @@ -69,85 +69,19 @@ export class Publishers implements PublisherBuilder { const typeDetector = makeDeprecatedLocationTypeDetector(config); const publishers = new Publishers(typeDetector); - const githubConfig = config.getOptionalConfig('scaffolder.github'); - if (githubConfig) { - try { - const githubPublisher = new GithubPublisher(config, { logger }); - publishers.register('file', githubPublisher); - publishers.register('github', githubPublisher); - } catch (e) { - const providerName = 'github'; - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, - ); - } + const githubPublisher = new GithubPublisher(config, { logger }); + publishers.register('file', githubPublisher); + publishers.register('github', githubPublisher); - logger.warn( - `Skipping ${providerName} scaffolding provider, ${e.message}`, - ); - } - } + const gitLabPublisher = new GitlabPublisher(config, { logger }); + publishers.register('gitlab', gitLabPublisher); - const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab'); - if (gitLabConfig) { - try { - const gitLabPublisher = new GitlabPublisher(config, { logger }); - publishers.register('gitlab', gitLabPublisher); - publishers.register('gitlab/api', gitLabPublisher); - } catch (e) { - const providerName = 'gitlab'; - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, - ); - } + const azurePublisher = new AzurePublisher(config, { logger }); + publishers.register('azure', azurePublisher); - logger.warn( - `Skipping ${providerName} scaffolding provider, ${e.message}`, - ); - } - } + const bitbucketPublisher = new BitbucketPublisher(config, { logger }); + publishers.register('bitbucket', bitbucketPublisher); - const azureConfig = config.getOptionalConfig('scaffolder.azure'); - if (azureConfig) { - try { - const azurePublisher = new AzurePublisher(config, { logger }); - publishers.register('azure/api', azurePublisher); - } catch (e) { - const providerName = 'azure'; - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, - ); - } - - logger.warn( - `Skipping ${providerName} scaffolding provider, ${e.message}`, - ); - } - } - - const bitbucketConfig = config.getOptionalConfig( - 'scaffolder.bitbucket.api', - ); - if (bitbucketConfig) { - try { - const bitbucketPublisher = new BitbucketPublisher(config, { logger }); - publishers.register('bitbucket', bitbucketPublisher); - } catch (e) { - const providerName = 'bitbucket'; - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, - ); - } - - logger.warn( - `Skipping ${providerName} scaffolding provider, ${e.message}`, - ); - } - } return publishers; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/types.ts index 56111337fe..7f12eb71db 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/types.ts @@ -17,6 +17,5 @@ export type RemoteProtocol = | 'file' | 'github' | 'gitlab' - | 'gitlab/api' - | 'azure/api' + | 'azure' | 'bitbucket'; From d1fa548bd1a20f1c5d61476503d7b3b85936b1b5 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 08:56:13 +0100 Subject: [PATCH 21/44] Rename azure/api to azure --- plugins/scaffolder-backend/src/service/router.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 4d610299eb..bbc054e519 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -47,7 +47,7 @@ describe('createRouter - working directory', () => { const mockPreparer = { prepare: mockPrepare, }; - mockPreparers.register('azure/api', mockPreparer); + mockPreparers.register('azure', mockPreparer); }); beforeEach(() => { @@ -65,7 +65,7 @@ describe('createRouter - working directory', () => { kind: 'Template', metadata: { annotations: { - 'backstage.io/managed-by-location': 'azure/api:dev.azure.com', + 'backstage.io/managed-by-location': 'azure:dev.azure.com', }, }, spec: { From 5880c280d6d9fde798e5495c909150d7646bfaa2 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 10:19:53 +0100 Subject: [PATCH 22/44] Use correct versions --- plugins/scaffolder-backend/package.json | 4 ++-- yarn.lock | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index ffce3590d0..134f63f96a 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -32,10 +32,10 @@ "@backstage/backend-common": "^0.4.3", "@backstage/catalog-model": "^0.6.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.1.5", + "@backstage/integration": "^0.2.0", "@gitbeaker/core": "^28.0.2", "@gitbeaker/node": "^28.0.2", - "@octokit/rest": "^18.0.0", + "@octokit/rest": "^18.0.12", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "azure-devops-node-api": "^10.1.1", diff --git a/yarn.lock b/yarn.lock index 9172d827f0..fa2f5aca5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2494,15 +2494,6 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/integration@^0.1.5": - version "0.1.5" - resolved "https://registry.npmjs.org/@backstage/integration/-/integration-0.1.5.tgz#281042b4c49939eea7d3dacb77345bb8a0d7ce01" - integrity sha512-ts+47TInn86SNUlEOSsC8t6iwJU4A9ACutLNpnlnqW1hVuAxEy9c+H1pp2UTe+bIuaop0XYvkVIQl7qYUF478Q== - dependencies: - "@backstage/config" "^0.1.2" - cross-fetch "^3.0.6" - git-url-parse "^11.4.3" - "@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" From 3c75afde45e23992410726325470276112efda0d Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 10:21:52 +0100 Subject: [PATCH 23/44] Remove stacktrace from logs --- plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts index fc11545e04..25f9b23e59 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts @@ -120,7 +120,6 @@ export class JobProcessor implements Processor { // Log to the current stage the error that occurred and fail the stage. stage.status = 'FAILED'; logger.error(`Stage failed with error: ${error.message}`); - logger.error(error.stack); // Throw the error so the job can be failed too. throw error; From 4a3a9ff637b127a44c58dc7d184370da9b529e57 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 10:24:28 +0100 Subject: [PATCH 24/44] Log stacktrace in debug --- plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts index 25f9b23e59..4ea0b4371b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts @@ -120,7 +120,7 @@ export class JobProcessor implements Processor { // Log to the current stage the error that occurred and fail the stage. stage.status = 'FAILED'; logger.error(`Stage failed with error: ${error.message}`); - + logger.debug(error.stack); // Throw the error so the job can be failed too. throw error; } finally { From a41a94b4b5a570604eafc0f26d09be56421b51fb Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 11:20:40 +0100 Subject: [PATCH 25/44] register preparers with fromConfig Co-authored-by: blam --- .../scaffolder/stages/prepare/azure.test.ts | 10 +++--- .../src/scaffolder/stages/prepare/azure.ts | 31 ++++++++++-------- .../stages/prepare/bitbucket.test.ts | 12 +++---- .../scaffolder/stages/prepare/bitbucket.ts | 28 ++++++++++------ .../scaffolder/stages/prepare/github.test.ts | 9 ++---- .../src/scaffolder/stages/prepare/github.ts | 27 +++++++++------- .../scaffolder/stages/prepare/gitlab.test.ts | 10 +++--- .../src/scaffolder/stages/prepare/gitlab.ts | 32 +++++++++++-------- .../scaffolder/stages/prepare/preparers.ts | 8 ++--- 9 files changed, 90 insertions(+), 77 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 896d91f4ed..d0860ea96c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -92,7 +92,6 @@ describe('AzurePreparer', () => { }, }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -113,7 +112,6 @@ describe('AzurePreparer', () => { ], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -126,7 +124,7 @@ describe('AzurePreparer', () => { }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + const preparer = new AzurePreparer(new ConfigReader({})); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -138,7 +136,7 @@ describe('AzurePreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + const preparer = new AzurePreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -151,7 +149,7 @@ describe('AzurePreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { @@ -164,7 +162,7 @@ describe('AzurePreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index d39b1f9e2d..001626f417 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -32,26 +32,31 @@ export class AzurePreparer implements PreparerBase { private readonly integrations: AzureIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config, { logger }: { logger: Logger }) { + const integrations = readAzureIntegrationConfigs( + config.getOptionalConfigArray('integrations.azure') ?? [], + ); + + if ( + config.getOptionalString('scaffolder.azure.api.token') && + !integrations.length + ) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", + 'Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + return new AzurePreparer(config); + } + + constructor(config: Config) { this.integrations = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.scaffolderToken = config.getOptionalString( 'scaffolder.azure.api.token', ); - - if (this.scaffolderToken) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } } async prepare( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index d55ca7a01d..05b64ca777 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -80,7 +80,7 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + const preparer = new BitbucketPreparer(new ConfigReader({})); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', @@ -101,7 +101,6 @@ describe('BitbucketPreparer', () => { ], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -114,7 +113,7 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + const preparer = new BitbucketPreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ @@ -124,7 +123,7 @@ describe('BitbucketPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + const preparer = new BitbucketPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), @@ -147,7 +146,6 @@ describe('BitbucketPreparer', () => { }, }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -172,7 +170,6 @@ describe('BitbucketPreparer', () => { ], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -197,7 +194,6 @@ describe('BitbucketPreparer', () => { ], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -210,7 +206,7 @@ describe('BitbucketPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + const preparer = new BitbucketPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 26d9b476ee..7cd724cbc1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -32,27 +32,35 @@ export class BitbucketPreparer implements PreparerBase { private readonly privateToken: string; private readonly username: string; private readonly integrations: BitbucketIntegrationConfig[]; - constructor(config: Config, { logger }: { logger: Logger }) { - this.integrations = readBitbucketIntegrationConfigs( + + static fromConfig(config: Config, { logger }: { logger: Logger }) { + const integrations = readBitbucketIntegrationConfigs( config.getOptionalConfigArray('integrations.bitbucket') ?? [], ); - if (!this.integrations.length) { + const user = config.getOptionalString('scaffolder.bitbucket.api.username'); + const token = config.getOptionalString('scaffolder.bitbucket.api.token'); + const password = config.getOptionalString( + 'scaffolder.bitbucket.api.appPassword', + ); + + if (!integrations && (user || token || password)) { logger.warn( - 'Integrations for BitBucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + "DEPRECATION: Setting credentials under 'scaffolder.bitbucket.api' will not be respected in future releases. Please consider using integrations config instead", + 'Please migrate to using integrations config and specifying tokens under hostnames', ); } + return new BitbucketPreparer(config); + } + constructor(config: Config) { + this.integrations = readBitbucketIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucket') ?? [], + ); this.username = config.getOptionalString('scaffolder.bitbucket.api.username') ?? ''; this.privateToken = config.getOptionalString('scaffolder.bitbucket.api.token') ?? ''; - - if (this.username || this.privateToken) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.bitbucket.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } } async prepare( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 122a1abb4c..68178610b5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -87,7 +87,6 @@ describe('GitHubPreparer', () => { }, }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -98,7 +97,7 @@ describe('GitHubPreparer', () => { }); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new GithubPreparer(new ConfigReader({}), { logger }); + const preparer = new GithubPreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -110,7 +109,7 @@ describe('GitHubPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(new ConfigReader({}), { logger }); + const preparer = new GithubPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), @@ -121,7 +120,7 @@ describe('GitHubPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(new ConfigReader({}), { logger }); + const preparer = new GithubPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', @@ -142,7 +141,6 @@ describe('GitHubPreparer', () => { }, }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -161,7 +159,6 @@ describe('GitHubPreparer', () => { github: [{ host: 'github.com', token: 'fake-me' }], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 81b212baaf..6ba341ad2c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -32,24 +32,29 @@ export class GithubPreparer implements PreparerBase { private readonly integrations: GitHubIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - constructor(config: Config, { logger }: { logger: Logger }) { - this.integrations = readGitHubIntegrationConfigs( + static fromConfig(config: Config, { logger }: { logger: Logger }) { + const integrations = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for Github in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - - this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); - - if (this.scaffolderToken) { + if ( + config.getOptionalString('scaffolder.github.token') && + !integrations.length + ) { logger.warn( "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", + 'Please migrate to using integrations config and specifying tokens under hostnames', ); } + + return new GithubPreparer(config); + } + + constructor(config: Config) { + this.integrations = readGitHubIntegrationConfigs( + config.getOptionalConfigArray('integrations.github') ?? [], + ); + this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); } async prepare( 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 c1e42920ae..f9e79e6c71 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -80,7 +80,7 @@ describe('GitLabPreparer', () => { ['gitlab'].forEach(protocol => { it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); + const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockEntityWithProtocol(protocol); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -103,7 +103,6 @@ describe('GitLabPreparer', () => { ], }, }), - { logger }, ); mockEntity = mockEntityWithProtocol(protocol); @@ -123,7 +122,6 @@ describe('GitLabPreparer', () => { gitlab: { api: { token: 'fake-token' } }, }, }), - { logger }, ); mockEntity = mockEntityWithProtocol(protocol); @@ -137,7 +135,7 @@ describe('GitLabPreparer', () => { }); it(`calls the clone command with the correct arguments for a repository when no path is provided using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); + const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockEntityWithProtocol(protocol); delete mockEntity.spec.path; @@ -150,7 +148,7 @@ describe('GitLabPreparer', () => { }); it(`return the temp directory with the path to the folder if it is specified using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); + const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockEntityWithProtocol(protocol); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { @@ -162,7 +160,7 @@ describe('GitLabPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); + const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 7f9d9c7d3a..4e5cdb9cc7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -32,26 +32,32 @@ export class GitlabPreparer implements PreparerBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config, { logger }: { logger: Logger }) { + const integrations = readGitLabIntegrationConfigs( + config.getOptionalConfigArray('integrations.gitlab') ?? [], + ); + + if ( + config.getOptionalString('scaffolder.gitlab.api.token') && + !integrations.length + ) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.gitlab.token' will not be respected in future releases. Please consider using integrations config instead", + 'Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + return new GitlabPreparer(config); + } + + constructor(config: Config) { this.integrations = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.scaffolderToken = config.getOptionalString( 'scaffolder.gitlab.api.token', ); - - if (this.scaffolderToken) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } } async prepare( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index 251e8b0ae3..94d2706cd8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -78,10 +78,10 @@ export class Preparers implements PreparerBuilder { const preparers = new Preparers(typeDetector); const filePreparer = new FilePreparer(); - const gitlabPreparer = new GitlabPreparer(config, { logger }); - const azurePreparer = new AzurePreparer(config, { logger }); - const githubPreparer = new GithubPreparer(config, { logger }); - const bitbucketPreparer = new BitbucketPreparer(config, { logger }); + const gitlabPreparer = GitlabPreparer.fromConfig(config, { logger }); + const azurePreparer = AzurePreparer.fromConfig(config, { logger }); + const githubPreparer = GithubPreparer.fromConfig(config, { logger }); + const bitbucketPreparer = BitbucketPreparer.fromConfig(config, { logger }); preparers.register('file', filePreparer); preparers.register('gitlab', gitlabPreparer); From a532d9d124b20d836a211fab95354aee310041a8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 11:51:19 +0100 Subject: [PATCH 26/44] Add fromConfig remove logging --- packages/backend/src/plugins/scaffolder.ts | 2 +- .../src/scaffolder/stages/prepare/azure.ts | 10 ++---- .../scaffolder/stages/prepare/bitbucket.ts | 6 +--- .../src/scaffolder/stages/prepare/github.ts | 9 +---- .../src/scaffolder/stages/prepare/gitlab.ts | 9 +---- .../scaffolder/stages/publish/azure.test.ts | 2 -- .../src/scaffolder/stages/publish/azure.ts | 26 +++----------- .../stages/publish/bitbucket.test.ts | 6 ---- .../scaffolder/stages/publish/bitbucket.ts | 36 +++---------------- .../scaffolder/stages/publish/github.test.ts | 7 ---- .../src/scaffolder/stages/publish/github.ts | 24 +++---------- .../scaffolder/stages/publish/gitlab.test.ts | 2 -- .../src/scaffolder/stages/publish/gitlab.ts | 26 +++----------- .../stages/publish/publishers.test.ts | 19 +++------- .../scaffolder/stages/publish/publishers.ts | 13 +++---- 15 files changed, 32 insertions(+), 165 deletions(-) diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 5d36d508a5..70a961819f 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -38,7 +38,7 @@ export default async function createPlugin({ templaters.register('cra', craTemplater); const preparers = await Preparers.fromConfig(config, { logger }); - const publishers = await Publishers.fromConfig(config, { logger }); + const publishers = await Publishers.fromConfig(config); const dockerClient = new Docker(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index 001626f417..5064c8ff1e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -33,19 +33,13 @@ export class AzurePreparer implements PreparerBase { private readonly scaffolderToken: string | undefined; static fromConfig(config: Config, { logger }: { logger: Logger }) { - const integrations = readAzureIntegrationConfigs( - config.getOptionalConfigArray('integrations.azure') ?? [], - ); - - if ( - config.getOptionalString('scaffolder.azure.api.token') && - !integrations.length - ) { + if (config.getOptionalString('scaffolder.azure.api.token')) { logger.warn( "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", 'Please migrate to using integrations config and specifying tokens under hostnames', ); } + return new AzurePreparer(config); } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 7cd724cbc1..cce0689ac9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -34,17 +34,13 @@ export class BitbucketPreparer implements PreparerBase { private readonly integrations: BitbucketIntegrationConfig[]; static fromConfig(config: Config, { logger }: { logger: Logger }) { - const integrations = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [], - ); - const user = config.getOptionalString('scaffolder.bitbucket.api.username'); const token = config.getOptionalString('scaffolder.bitbucket.api.token'); const password = config.getOptionalString( 'scaffolder.bitbucket.api.appPassword', ); - if (!integrations && (user || token || password)) { + if (user || token || password) { logger.warn( "DEPRECATION: Setting credentials under 'scaffolder.bitbucket.api' will not be respected in future releases. Please consider using integrations config instead", 'Please migrate to using integrations config and specifying tokens under hostnames', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 6ba341ad2c..49858adf42 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -33,14 +33,7 @@ export class GithubPreparer implements PreparerBase { private readonly scaffolderToken: string | undefined; static fromConfig(config: Config, { logger }: { logger: Logger }) { - const integrations = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - - if ( - config.getOptionalString('scaffolder.github.token') && - !integrations.length - ) { + if (config.getOptionalString('scaffolder.github.token')) { logger.warn( "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", 'Please migrate to using integrations config and specifying tokens under hostnames', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 4e5cdb9cc7..49168deeff 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -33,14 +33,7 @@ export class GitlabPreparer implements PreparerBase { private readonly scaffolderToken: string | undefined; static fromConfig(config: Config, { logger }: { logger: Logger }) { - const integrations = readGitLabIntegrationConfigs( - config.getOptionalConfigArray('integrations.gitlab') ?? [], - ); - - if ( - config.getOptionalString('scaffolder.gitlab.api.token') && - !integrations.length - ) { + if (config.getOptionalString('scaffolder.gitlab.api.token')) { logger.warn( "DEPRECATION: Using the token format under 'scaffolder.gitlab.token' will not be respected in future releases. Please consider using integrations config instead", 'Please migrate to using integrations config and specifying tokens under hostnames', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts index 9aaee4bcb8..2784a1f9bb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts @@ -51,7 +51,6 @@ describe('Azure Publisher', () => { }, }, }), - { logger }, ); mockGitClient.createRepository.mockResolvedValue({ remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', @@ -106,7 +105,6 @@ describe('Azure Publisher', () => { ], }, }), - { logger }, ); mockGitClient.createRepository.mockResolvedValue({ remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index 4172f28f6b..d3f900d706 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -19,7 +19,6 @@ import { IGitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { Config } from '@backstage/config'; import { initRepoAndPush } from './helpers'; -import { Logger } from 'winston'; import { AzureIntegrationConfig, readAzureIntegrationConfigs, @@ -32,33 +31,18 @@ export class AzurePublisher implements PublisherBase { private readonly apiBaseUrl?: string; private readonly token?: string; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config) { + return new AzurePublisher(config); + } + + constructor(config: Config) { this.integrations = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.token = config.getOptionalString('scaffolder.azure.api.token'); - if (this.token) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.github.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.apiBaseUrl = config.getOptionalString('scaffolder.azure.api.baseUrl'); - - if (this.apiBaseUrl) { - logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.azure.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", - ); - } } - async publish({ values, directory, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts index 6cfcc7d69e..c4ef86786e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts @@ -71,9 +71,6 @@ describe('Bitbucket Publisher', () => { ], }, }), - { - logger: getVoidLogger(), - }, ); const result = await publisher.publish({ @@ -140,9 +137,6 @@ describe('Bitbucket Publisher', () => { ], }, }), - { - logger: getVoidLogger(), - }, ); const result = await publisher.publish({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index 4cc7654136..ea21f89834 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -22,7 +22,6 @@ import { BitbucketIntegrationConfig, readBitbucketIntegrationConfigs, } from '@backstage/integration'; -import { Logger } from 'winston'; import gitUrlParse from 'git-url-parse'; // TODO(blam): We should probably start to use a bitbucket client here that we can change @@ -35,48 +34,21 @@ export class BitbucketPublisher implements PublisherBase { private readonly appPassword?: string; private readonly integrations: BitbucketIntegrationConfig[]; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config) { + return new BitbucketPublisher(config); + } + constructor(config: Config) { this.integrations = readBitbucketIntegrationConfigs( config.getOptionalConfigArray('integrations.bitbucket') ?? [], ); - - if (!this.integrations.length) { - logger.warn( - 'Integrations for Bitbucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.token = config.getOptionalString('scaffolder.bitbucket.api.token'); - if (this.token) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.bitbucket.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.host = config.getOptionalString('scaffolder.bitbucket.api.host'); - if (this.host) { - logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.host' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.username = config.getOptionalString( 'scaffolder.bitbucket.api.username', ); - if (this.username) { - logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.username' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.appPassword = config.getOptionalString( 'scaffolder.bitbucket.api.appPassword', ); - if (this.appPassword) { - logger.warn( - "DEPRECATION: Using the appPassword format under 'scaffolder.bitbucket.api.appassword' will not be respected in future releases. Please consider using integrations config instead", - ); - } } async publish({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index 7402dc5ef5..c5b8187bc4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -49,9 +49,6 @@ describe('GitHub Publisher', () => { ], }, }), - { - logger, - }, ); describe('publish: createRemoteInGithub', () => { @@ -211,7 +208,6 @@ describe('GitHub Publisher', () => { }, }, }), - { logger }, ); it('creates a private repository in the organization with visibility set to internal', async () => { @@ -273,9 +269,6 @@ describe('GitHub Publisher', () => { }, }, }), - { - logger, - }, ); it('creates a private repository', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 902dc72a77..a9004966a4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -17,7 +17,6 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; import { Config } from '@backstage/config'; -import { Logger } from 'winston'; import { GitHubIntegrationConfig, readGitHubIntegrationConfigs, @@ -32,35 +31,20 @@ export class GithubPublisher implements PublisherBase { private readonly apiBaseUrl: string | undefined; private readonly repoVisibility: RepoVisibilityOptions; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config) { + return new GithubPublisher(config); + } + constructor(config: Config) { this.integrations = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for GitHub in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.scaffolderToken = config.getOptionalString( 'scaffolder.github.api.token', ); this.apiBaseUrl = config.getOptionalString('scaffolder.github.api.baseUrl'); - if (this.scaffolderToken) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.github.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } - - if (this.apiBaseUrl) { - logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.github.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.repoVisibility = (config.getOptionalString( 'scaffolder.github.visibility', ) ?? 'public') as RepoVisibilityOptions; 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 67887f579f..fe997f295d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -61,7 +61,6 @@ describe('GitLab Publisher', () => { ], }, }), - { logger }, ); mockGitlabClient.Namespaces.show.mockResolvedValue({ @@ -109,7 +108,6 @@ describe('GitLab Publisher', () => { ], }, }), - { logger }, ); mockGitlabClient.Namespaces.show.mockResolvedValue({}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index 71a38f1079..76167cf20f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -17,7 +17,6 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { Gitlab } from '@gitbeaker/node'; import { Config } from '@backstage/config'; -import { Logger } from 'winston'; import { initRepoAndPush } from './helpers'; import gitUrlParse from 'git-url-parse'; @@ -30,37 +29,20 @@ export class GitlabPublisher implements PublisherBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; private readonly apiBaseUrl: string | undefined; - private readonly logger: Logger; - constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; + constructor(config: Config) { this.integrations = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); - - if (!this.integrations.length) { - this.logger.warn( - 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.scaffolderToken = config.getOptionalString( 'scaffolder.gitlab.api.token', ); this.apiBaseUrl = config.getOptionalString('scaffolder.gitlab.api.baseUrl'); + } - if (this.scaffolderToken) { - this.logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } - - if (this.apiBaseUrl) { - this.logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.gitlab.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", - ); - } + static fromConfig(config: Config) { + return new GitlabPublisher(config); } async publish({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts index fdbc283f67..869d1fcd03 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts @@ -40,9 +40,7 @@ describe('Publishers', () => { }); it('should return the correct preparer when the source matches for github', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({}), { - logger: getVoidLogger(), - }); + const publishers = await Publishers.fromConfig(new ConfigReader({})); expect( publishers.get('https://github.com/org/repo', { @@ -52,9 +50,7 @@ describe('Publishers', () => { }); it('should return the correct preparer when the source matches for azure', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({}), { - logger: getVoidLogger(), - }); + const publishers = await Publishers.fromConfig(new ConfigReader({})); expect( publishers.get('https://dev.azure.com/org/project/_git/repo', { @@ -64,9 +60,7 @@ describe('Publishers', () => { }); it('should return the correct preparer when the source matches for bitbucket', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({}), { - logger: getVoidLogger(), - }); + const publishers = await Publishers.fromConfig(new ConfigReader({})); expect( publishers.get('https://bitbucket.org/owner/repo', { @@ -76,9 +70,7 @@ describe('Publishers', () => { }); it('should return the correct preparer when the source matches for gitlab', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({}), { - logger: getVoidLogger(), - }); + const publishers = await Publishers.fromConfig(new ConfigReader({})); expect( publishers.get('https://gitlab.com/owner/repo', { @@ -96,9 +88,6 @@ describe('Publishers', () => { ], }, }), - { - logger: getVoidLogger(), - }, ); expect( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 1fafc33c68..2527ad12b4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -62,24 +62,21 @@ export class Publishers implements PublisherBuilder { return publisher; } - static async fromConfig( - config: Config, - { logger }: { logger: Logger }, - ): Promise { + static async fromConfig(config: Config): Promise { const typeDetector = makeDeprecatedLocationTypeDetector(config); const publishers = new Publishers(typeDetector); - const githubPublisher = new GithubPublisher(config, { logger }); + const githubPublisher = GithubPublisher.fromConfig(config); publishers.register('file', githubPublisher); publishers.register('github', githubPublisher); - const gitLabPublisher = new GitlabPublisher(config, { logger }); + const gitLabPublisher = GitlabPublisher.fromConfig(config); publishers.register('gitlab', gitLabPublisher); - const azurePublisher = new AzurePublisher(config, { logger }); + const azurePublisher = AzurePublisher.fromConfig(config); publishers.register('azure', azurePublisher); - const bitbucketPublisher = new BitbucketPublisher(config, { logger }); + const bitbucketPublisher = BitbucketPublisher.fromConfig(config); publishers.register('bitbucket', bitbucketPublisher); return publishers; From d095cbf3d356759a8478e5e310073685141004e6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 13:03:34 +0100 Subject: [PATCH 27/44] Delete log argument when calling fromConfig --- .../default-app/packages/backend/src/plugins/scaffolder.ts | 2 +- 1 file changed, 1 insertion(+), 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 196d48ec78..a0ee16d4ba 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 @@ -22,7 +22,7 @@ export default async function createPlugin({ templaters.register('cra', craTemplater); const preparers = await Preparers.fromConfig(config, { logger }); - const publishers = await Publishers.fromConfig(config, { logger }); + const publishers = await Publishers.fromConfig(config); const dockerClient = new Docker(); From 9d0733de1f358197e33beca39f245c41ea702d77 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 13:19:14 +0100 Subject: [PATCH 28/44] Cleanup gitlab tests --- .../scaffolder/stages/prepare/gitlab.test.ts | 183 +++++++++--------- 1 file changed, 91 insertions(+), 92 deletions(-) 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 f9e79e6c71..af9d2bac42 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -27,12 +27,13 @@ import { import { ConfigReader } from '@backstage/config'; import { getVoidLogger, Git } from '@backstage/backend-common'; -const mockEntityWithProtocol = (protocol: string): TemplateEntityV1alpha1 => ({ +const mockTemplate = (): TemplateEntityV1alpha1 => ({ apiVersion: 'backstage.io/v1alpha1', kind: 'Template', metadata: { annotations: { - [LOCATION_ANNOTATION]: `${protocol}:https://gitlab.com/benjdlambert/backstage-graphql-template/-/blob/master/template.yaml`, + [LOCATION_ANNOTATION]: + 'gitlab:https://gitlab.com/benjdlambert/backstage-graphql-template/-/blob/master/template.yaml', }, name: 'graphql-starter', title: 'GraphQL Service', @@ -78,98 +79,96 @@ describe('GitLabPreparer', () => { jest.clearAllMocks(); }); - ['gitlab'].forEach(protocol => { - it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); - mockEntity = mockEntityWithProtocol(protocol); + it(`calls the clone command with the correct arguments for a repository`, async () => { + const preparer = new GitlabPreparer(new ConfigReader({})); + mockEntity = mockTemplate(); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', - dir: expect.any(String), - }); - }); - - it(`calls the clone command with the correct arguments if an access token is provided in integrations for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer( - new ConfigReader({ - integrations: { - gitlab: [ - { - host: 'gitlab.com', - token: 'fake-token', - }, - ], - }, - }), - ); - mockEntity = mockEntityWithProtocol(protocol); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'oauth2', - password: 'fake-token', - }); - }); - - it(`calls the clone command with the correct arguments if an access token is provided in scaffolder for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer( - new ConfigReader({ - scaffolder: { - gitlab: { api: { token: 'fake-token' } }, - }, - }), - ); - mockEntity = mockEntityWithProtocol(protocol); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'oauth2', - password: 'fake-token', - }); - }); - - it(`calls the clone command with the correct arguments for a repository when no path is provided using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); - mockEntity = mockEntityWithProtocol(protocol); - delete mockEntity.spec.path; - - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', - dir: expect.any(String), - }); - }); - - it(`return the temp directory with the path to the folder if it is specified using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); - mockEntity = mockEntityWithProtocol(protocol); - mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); - expect(response.split('\\').join('/')).toMatch( - /\/template\/test\/1\/2\/3$/, - ); - }); - - it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); - mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - workingDirectory: '/workDir', - logger: getVoidLogger(), - }); - - expect(response.split('\\').join('/')).toMatch( - /\/workDir\/graphql-starter-static\/template\/test\/1\/2\/3$/, - ); + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', + dir: expect.any(String), }); }); + + it(`calls the clone command with the correct arguments if an access token is provided in integrations for a repository`, async () => { + const preparer = new GitlabPreparer( + new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'fake-token', + }, + ], + }, + }), + ); + mockEntity = mockTemplate(); + + await preparer.prepare(mockEntity, { logger }); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'oauth2', + password: 'fake-token', + }); + }); + + it(`calls the clone command with the correct arguments if an access token is provided in scaffolder for a repository`, async () => { + const preparer = new GitlabPreparer( + new ConfigReader({ + scaffolder: { + gitlab: { api: { token: 'fake-token' } }, + }, + }), + ); + mockEntity = mockTemplate(); + + await preparer.prepare(mockEntity, { logger }); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'oauth2', + password: 'fake-token', + }); + }); + + it(`calls the clone command with the correct arguments for a repository when no path is provided`, async () => { + const preparer = new GitlabPreparer(new ConfigReader({})); + mockEntity = mockTemplate(); + delete mockEntity.spec.path; + + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', + dir: expect.any(String), + }); + }); + + it(`return the temp directory with the path to the folder if it is specified`, async () => { + const preparer = new GitlabPreparer(new ConfigReader({})); + mockEntity = mockTemplate(); + mockEntity.spec.path = './template/test/1/2/3'; + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); + expect(response.split('\\').join('/')).toMatch( + /\/template\/test\/1\/2\/3$/, + ); + }); + + it('return the working directory with the path to the folder if it is specified', async () => { + const preparer = new GitlabPreparer(new ConfigReader({})); + mockEntity.spec.path = './template/test/1/2/3'; + const response = await preparer.prepare(mockEntity, { + workingDirectory: '/workDir', + logger: getVoidLogger(), + }); + + expect(response.split('\\').join('/')).toMatch( + /\/workDir\/graphql-starter-static\/template\/test\/1\/2\/3$/, + ); + }); }); From 544139ed16c31a0791428a9d33d5b4b596c9616a Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 15:31:07 +0100 Subject: [PATCH 29/44] Refactor preparers --- .../scaffolder/stages/prepare/azure.test.ts | 35 +-- .../src/scaffolder/stages/prepare/azure.ts | 53 +---- .../stages/prepare/bitbucket.test.ts | 98 ++------ .../scaffolder/stages/prepare/bitbucket.ts | 82 ++----- .../scaffolder/stages/prepare/github.test.ts | 60 ++--- .../src/scaffolder/stages/prepare/github.ts | 40 +--- .../scaffolder/stages/prepare/gitlab.test.ts | 40 +--- .../src/scaffolder/stages/prepare/gitlab.ts | 43 +--- .../stages/prepare/preparers.test.ts | 220 ++++++++++-------- .../scaffolder/stages/prepare/preparers.ts | 99 ++++---- .../src/scaffolder/stages/prepare/types.ts | 4 +- .../scaffolder/stages/publish/azure.test.ts | 74 +----- .../src/scaffolder/stages/publish/azure.ts | 70 ++---- .../scaffolder-backend/src/service/router.ts | 9 +- 14 files changed, 297 insertions(+), 630 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index d0860ea96c..8dd72c35e9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -26,7 +26,6 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { getVoidLogger, Git } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; describe('AzurePreparer', () => { const mockGitClient = { @@ -80,20 +79,13 @@ describe('AzurePreparer', () => { }; }); + const preparer = AzurePreparer.fromConfig({ + host: 'dev.azure.com', + token: 'fake-azure-token', + }); + // TODO(blam): Here's a test that will fail when the deprecation is complete it('calls the clone command with deprecated token', async () => { - const preparer = new AzurePreparer( - new ConfigReader({ - scaffolder: { - azure: { - api: { - token: 'fake-azure-token', - }, - }, - }, - }), - ); - await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ @@ -104,28 +96,16 @@ describe('AzurePreparer', () => { }); it('calls the clone command with token from integrations config', async () => { - const preparer = new AzurePreparer( - new ConfigReader({ - integrations: { - azure: [ - { host: 'dev.azure.com', token: 'fake-azure-token-integration' }, - ], - }, - }), - ); - await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, - password: 'fake-azure-token-integration', + password: 'fake-azure-token', username: 'notempty', }); }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ @@ -136,7 +116,6 @@ describe('AzurePreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -149,7 +128,6 @@ describe('AzurePreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { @@ -162,7 +140,6 @@ describe('AzurePreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index 5064c8ff1e..df127d22e7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -18,54 +18,26 @@ import fs from 'fs-extra'; import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; -import { InputError, Git } from '@backstage/backend-common'; +import { Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; import parseGitUrl from 'git-url-parse'; -import { Config } from '@backstage/config'; -import { Logger } from 'winston'; -import { - readAzureIntegrationConfigs, - AzureIntegrationConfig, -} from '@backstage/integration'; +import { AzureIntegrationConfig } from '@backstage/integration'; export class AzurePreparer implements PreparerBase { - private readonly integrations: AzureIntegrationConfig[]; - private readonly scaffolderToken: string | undefined; - - static fromConfig(config: Config, { logger }: { logger: Logger }) { - if (config.getOptionalString('scaffolder.azure.api.token')) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", - 'Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - - return new AzurePreparer(config); + static fromConfig(config: AzureIntegrationConfig) { + return new AzurePreparer(config.token); } - constructor(config: Config) { - this.integrations = readAzureIntegrationConfigs( - config.getOptionalConfigArray('integrations.azure') ?? [], - ); - - this.scaffolderToken = config.getOptionalString( - 'scaffolder.azure.api.token', - ); - } + constructor(private readonly token?: string) {} async prepare( template: TemplateEntityV1alpha1, opts: PreparerOptions, ): Promise { - const { protocol, location } = parseLocationAnnotation(template); + const { location } = parseLocationAnnotation(template); const workingDirectory = opts.workingDirectory ?? os.tmpdir(); const logger = opts.logger; - if (!['azure', 'url'].includes(protocol)) { - throw new InputError( - `Wrong location protocol: ${protocol}, should be 'url'`, - ); - } const templateId = template.metadata.name; const parsedGitLocation = parseGitUrl(location); @@ -79,13 +51,11 @@ export class AzurePreparer implements PreparerBase { template.spec.path ?? '.', ); - const token = this.getToken(parsedGitLocation.resource); - // Username can be anything but the empty string according to: // https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat - const git = token + const git = this.token ? Git.fromAuth({ - password: token, + password: this.token, username: 'notempty', logger, }) @@ -98,11 +68,4 @@ export class AzurePreparer implements PreparerBase { return path.resolve(tempDir, templateDirectory); } - - private getToken(host: string): string | undefined { - return ( - this.scaffolderToken || - this.integrations.find(c => c.host === host)?.token - ); - } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index 05b64ca777..ff74fffdd7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -26,7 +26,6 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { getVoidLogger, Git } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; describe('BitbucketPreparer', () => { let mockEntity: TemplateEntityV1alpha1; @@ -79,8 +78,13 @@ describe('BitbucketPreparer', () => { }; }); + const preparer = BitbucketPreparer.fromConfig({ + host: 'bitbucket.org', + username: 'fake-user', + appPassword: 'fake-password', + }); + it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', @@ -89,20 +93,11 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments if an app password is provided for a repository', async () => { - const preparer = new BitbucketPreparer( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - username: 'fake-user', - appPassword: 'fake-password', - }, - ], - }, - }), - ); - + const preparer = BitbucketPreparer.fromConfig({ + host: 'bitbucket.org', + username: 'fake-user', + appPassword: 'fake-password', + }); await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ @@ -113,7 +108,6 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ @@ -123,7 +117,6 @@ describe('BitbucketPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), @@ -134,79 +127,22 @@ describe('BitbucketPreparer', () => { ); }); - it('calls the clone command with deprecated auth method', async () => { - const preparer = new BitbucketPreparer( - new ConfigReader({ - scaffolder: { - bitbucket: { - api: { - username: 'fakeusername', - token: 'faketoken', - }, - }, - }, - }), - ); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'fakeusername', - password: 'faketoken', + it('calls the clone command with with token for auth method', async () => { + const preparer = BitbucketPreparer.fromConfig({ + host: 'bitbucket.org', + token: 'fake-token', }); - }); - - it('calls the clone command with integrations config for auth method', async () => { - const preparer = new BitbucketPreparer( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - username: 'asd3', - token: 'faketoken', - }, - ], - }, - }), - ); await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, - username: 'asd3', - password: 'faketoken', - }); - }); - - it('calls the clone command with integrations config with appPassword for auth method', async () => { - const preparer = new BitbucketPreparer( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - username: 'asd3', - appPassword: 'myapppassword', - }, - ], - }, - }), - ); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'asd3', - password: 'myapppassword', + username: 'x-token-auth', + password: 'fake-token', }); }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index cce0689ac9..5aafb2730f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -20,44 +20,23 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; -import { - readBitbucketIntegrationConfigs, - BitbucketIntegrationConfig, -} from '@backstage/integration'; +import { BitbucketIntegrationConfig } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; -import { Config } from '@backstage/config'; -import { Logger } from 'winston'; export class BitbucketPreparer implements PreparerBase { - private readonly privateToken: string; - private readonly username: string; - private readonly integrations: BitbucketIntegrationConfig[]; - - static fromConfig(config: Config, { logger }: { logger: Logger }) { - const user = config.getOptionalString('scaffolder.bitbucket.api.username'); - const token = config.getOptionalString('scaffolder.bitbucket.api.token'); - const password = config.getOptionalString( - 'scaffolder.bitbucket.api.appPassword', + static fromConfig(config: BitbucketIntegrationConfig) { + return new BitbucketPreparer( + config.username, + config.token, + config.appPassword, ); - - if (user || token || password) { - logger.warn( - "DEPRECATION: Setting credentials under 'scaffolder.bitbucket.api' will not be respected in future releases. Please consider using integrations config instead", - 'Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - return new BitbucketPreparer(config); } - constructor(config: Config) { - this.integrations = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [], - ); - this.username = - config.getOptionalString('scaffolder.bitbucket.api.username') ?? ''; - this.privateToken = - config.getOptionalString('scaffolder.bitbucket.api.token') ?? ''; - } + constructor( + private readonly username?: string, + private readonly token?: string, + private readonly appPassword?: string, + ) {} async prepare( template: TemplateEntityV1alpha1, @@ -88,8 +67,7 @@ export class BitbucketPreparer implements PreparerBase { const checkoutLocation = path.resolve(tempDir, templateDirectory); - const auth = this.getAuth(repo.resource); - + const auth = this.getAuth(); const git = auth ? Git.fromAuth({ ...auth, @@ -105,36 +83,18 @@ export class BitbucketPreparer implements PreparerBase { return checkoutLocation; } - private getAuth( - host: string, - ): { username: string; password: string } | undefined { - if (this.username && this.privateToken) { - return { username: this.username, password: this.privateToken }; + private getAuth(): { username: string; password: string } | undefined { + if (this.username && this.appPassword) { + return { username: this.username, password: this.appPassword }; } - const bitbucketIntegrationConfig = this.integrations.find( - c => c.host === host, - ); - - if (!bitbucketIntegrationConfig) { - return undefined; + if (this.token) { + return { + username: 'x-token-auth', + password: this.token! || this.appPassword!, + }; } - if ( - !bitbucketIntegrationConfig.username || - !( - bitbucketIntegrationConfig.token || - bitbucketIntegrationConfig.appPassword - ) - ) { - return undefined; - } - - return { - username: bitbucketIntegrationConfig.username, - password: - bitbucketIntegrationConfig.token! || - bitbucketIntegrationConfig.appPassword!, - }; + return undefined; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 68178610b5..b43545f5fc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -26,7 +26,6 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { getVoidLogger, Git } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; describe('GitHubPreparer', () => { let mockEntity: TemplateEntityV1alpha1; @@ -78,17 +77,13 @@ describe('GitHubPreparer', () => { }, }; }); - it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new GithubPreparer( - new ConfigReader({ - scaffolder: { - github: { - token: 'fake-token', - }, - }, - }), - ); + const preparer = GithubPreparer.fromConfig({ + host: 'github.com', + token: 'fake-token', + }); + + it('calls the clone command with the correct arguments for a repository', async () => { await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ @@ -96,8 +91,8 @@ describe('GitHubPreparer', () => { dir: expect.any(String), }); }); + it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new GithubPreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -109,7 +104,10 @@ describe('GitHubPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(new ConfigReader({})); + const preparer = GithubPreparer.fromConfig({ + host: 'github.com', + token: 'fake-token', + }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), @@ -120,7 +118,11 @@ describe('GitHubPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(new ConfigReader({})); + const preparer = GithubPreparer.fromConfig({ + host: 'github.com', + token: 'fake-token', + }); + mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', @@ -132,17 +134,7 @@ describe('GitHubPreparer', () => { ); }); - it('calls the clone command with deprecated token', async () => { - const preparer = new GithubPreparer( - new ConfigReader({ - scaffolder: { - github: { - token: 'fake-token', - }, - }, - }), - ); - + it('calls the clone command with token', async () => { await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ @@ -151,22 +143,4 @@ describe('GitHubPreparer', () => { password: 'x-oauth-basic', }); }); - - it('calls the clone command with token from integrations config', async () => { - const preparer = new GithubPreparer( - new ConfigReader({ - integrations: { - github: [{ host: 'github.com', token: 'fake-me' }], - }, - }), - ); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'fake-me', - password: 'x-oauth-basic', - }); - }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 49858adf42..e0d669c211 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -20,35 +20,15 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; -import { Logger } from 'winston'; import parseGitUrl from 'git-url-parse'; -import { Config } from '@backstage/config'; -import { - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, -} from '@backstage/integration'; +import { GitHubIntegrationConfig } from '@backstage/integration'; export class GithubPreparer implements PreparerBase { - private readonly integrations: GitHubIntegrationConfig[]; - private readonly scaffolderToken: string | undefined; - - static fromConfig(config: Config, { logger }: { logger: Logger }) { - if (config.getOptionalString('scaffolder.github.token')) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", - 'Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - - return new GithubPreparer(config); + static fromConfig(config: GitHubIntegrationConfig) { + return new GithubPreparer(config.token); } - constructor(config: Config) { - this.integrations = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); - } + constructor(private readonly token?: string) {} async prepare( template: TemplateEntityV1alpha1, @@ -78,11 +58,9 @@ export class GithubPreparer implements PreparerBase { const checkoutLocation = path.resolve(tempDir, templateDirectory); - const token = this.getToken(parsedGitLocation.resource); - - const git = token + const git = this.token ? Git.fromAuth({ - username: token, + username: this.token, password: 'x-oauth-basic', logger, }) @@ -95,10 +73,4 @@ export class GithubPreparer implements PreparerBase { return checkoutLocation; } - private getToken(host: string): string | undefined { - return ( - this.scaffolderToken || - this.integrations.find(c => c.host === host)?.token - ); - } } 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 af9d2bac42..b2f1ac7fd9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -78,9 +78,11 @@ describe('GitLabPreparer', () => { beforeEach(() => { jest.clearAllMocks(); }); - + const preparer = GitlabPreparer.fromConfig({ + host: 'gitlab.com', + token: 'fake-token', + }); it(`calls the clone command with the correct arguments for a repository`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockTemplate(); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -92,37 +94,6 @@ describe('GitLabPreparer', () => { }); it(`calls the clone command with the correct arguments if an access token is provided in integrations for a repository`, async () => { - const preparer = new GitlabPreparer( - new ConfigReader({ - integrations: { - gitlab: [ - { - host: 'gitlab.com', - token: 'fake-token', - }, - ], - }, - }), - ); - mockEntity = mockTemplate(); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'oauth2', - password: 'fake-token', - }); - }); - - it(`calls the clone command with the correct arguments if an access token is provided in scaffolder for a repository`, async () => { - const preparer = new GitlabPreparer( - new ConfigReader({ - scaffolder: { - gitlab: { api: { token: 'fake-token' } }, - }, - }), - ); mockEntity = mockTemplate(); await preparer.prepare(mockEntity, { logger }); @@ -135,7 +106,6 @@ describe('GitLabPreparer', () => { }); it(`calls the clone command with the correct arguments for a repository when no path is provided`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockTemplate(); delete mockEntity.spec.path; @@ -148,7 +118,6 @@ describe('GitLabPreparer', () => { }); it(`return the temp directory with the path to the folder if it is specified`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockTemplate(); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { @@ -160,7 +129,6 @@ describe('GitLabPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 49168deeff..0bdecab794 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -15,43 +15,20 @@ */ import { InputError, Git } from '@backstage/backend-common'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; -import { - GitLabIntegrationConfig, - readGitLabIntegrationConfigs, -} from '@backstage/integration'; +import { GitLabIntegrationConfig } from '@backstage/integration'; import fs from 'fs-extra'; import parseGitUrl from 'git-url-parse'; import os from 'os'; import path from 'path'; import { parseLocationAnnotation } from '../helpers'; import { PreparerBase, PreparerOptions } from './types'; -import { Logger } from 'winston'; export class GitlabPreparer implements PreparerBase { - private readonly integrations: GitLabIntegrationConfig[]; - private readonly scaffolderToken: string | undefined; - - static fromConfig(config: Config, { logger }: { logger: Logger }) { - if (config.getOptionalString('scaffolder.gitlab.api.token')) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.gitlab.token' will not be respected in future releases. Please consider using integrations config instead", - 'Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - - return new GitlabPreparer(config); + static fromConfig(config: GitLabIntegrationConfig) { + return new GitlabPreparer(config.token); } - constructor(config: Config) { - this.integrations = readGitLabIntegrationConfigs( - config.getOptionalConfigArray('integrations.gitlab') ?? [], - ); - - this.scaffolderToken = config.getOptionalString( - 'scaffolder.gitlab.api.token', - ); - } + constructor(private readonly token?: string) {} async prepare( template: TemplateEntityV1alpha1, @@ -79,10 +56,9 @@ export class GitlabPreparer implements PreparerBase { template.spec.path ?? '.', ); - const token = this.getToken(parsedGitLocation.resource); - const git = token + const git = this.token ? Git.fromAuth({ - password: token, + password: this.token, username: 'oauth2', logger, }) @@ -95,11 +71,4 @@ export class GitlabPreparer implements PreparerBase { return path.resolve(tempDir, templateDirectory); } - - private getToken(host: string): string | undefined { - return ( - this.scaffolderToken || - this.integrations.find(c => c.host === host)?.token - ); - } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts index 7c2d7e0a47..db6160c276 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts @@ -16,107 +16,143 @@ import { Preparers } from '.'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { FilePreparer } from './file'; +import { GithubPreparer } from './github'; +import { ConfigReader } from '@backstage/config'; describe('Preparers', () => { - const mockTemplate: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'file:/Users/bingo/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml', - }, - name: 'react-ssr-template', - title: 'React SSR Template', - description: - 'Next.js application skeleton for creating isomorphic web applications.', - uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - generation: 1, - }, - spec: { - templater: 'cookiecutter', - path: '.', - type: 'website', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - }, - }; - it('should throw an error when the preparer for the source location is not registered', () => { - const preparers = new Preparers(); + // const mockTemplate: TemplateEntityV1alpha1 = { + // apiVersion: 'backstage.io/v1alpha1', + // kind: 'Template', + // metadata: { + // annotations: { + // 'backstage.io/managed-by-location': + // 'file:/Users/bingo/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml', + // }, + // name: 'react-ssr-template', + // title: 'React SSR Template', + // description: + // 'Next.js application skeleton for creating isomorphic web applications.', + // uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', + // etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', + // generation: 1, + // }, + // spec: { + // templater: 'cookiecutter', + // path: '.', + // type: 'website', + // schema: { + // $schema: 'http://json-schema.org/draft-07/schema#', + // required: ['storePath', 'owner'], + // properties: { + // owner: { + // type: 'string', + // title: 'Owner', + // description: 'Who is going to own this component', + // }, + // storePath: { + // type: 'string', + // title: 'Store path', + // description: 'GitHub store path in org/repo format', + // }, + // }, + // }, + // }, + // }; + // it('should throw an error when the preparer for the source location is not registered', () => { + // const preparers = new Preparers(); - expect(() => preparers.get(mockTemplate)).toThrow( - expect.objectContaining({ - message: 'No preparer registered for type: "file"', + // expect(() => preparers.get(mockTemplate)).toThrow( + // expect.objectContaining({ + // message: 'No preparer registered for type: "file"', + // }), + // ); + // }); + // it('should return the correct preparer when the source matches', () => { + // const preparers = new Preparers(); + // const preparer = new FilePreparer(); + + // preparers.register('file', preparer); + + // expect(preparers.get(mockTemplate)).toBe(preparer); + // }); + + // it('should throw an error if the metadata tag does not exist in the entity', () => { + // const brokenTemplate: TemplateEntityV1alpha1 = { + // apiVersion: 'backstage.io/v1alpha1', + // kind: 'Template', + // metadata: { + // annotations: {}, + // name: 'react-ssr-template', + // title: 'React SSR Template', + // description: + // 'Next.js application skeleton for creating isomorphic web applications.', + // uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', + // etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', + // generation: 1, + // }, + // spec: { + // type: 'website', + // templater: 'cookiecutter', + // path: '.', + // schema: { + // $schema: 'http://json-schema.org/draft-07/schema#', + // required: ['storePath', 'owner'], + // properties: { + // owner: { + // type: 'string', + // title: 'Owner', + // description: 'Who is going to own this component', + // }, + // storePath: { + // type: 'string', + // title: 'Store path', + // description: 'GitHub store path in org/repo format', + // }, + // }, + // }, + // }, + // }; + + // const preparers = new Preparers(); + + // expect(() => preparers.get(brokenTemplate)).toThrow( + // expect.objectContaining({ + // message: expect.stringContaining('No location annotation provided'), + // }), + // ); + // }); + + it('should return the correct preparer based on the hostname', async () => { + const preparer = new GithubPreparer( + new ConfigReader({ + host: 'github.com', + apiBaseUrl: 'lols', + token: 'something else yo', }), ); - }); - it('should return the correct preparer when the source matches', () => { - const preparers = new Preparers(); - const preparer = new FilePreparer(); - - preparers.register('file', preparer); - - expect(preparers.get(mockTemplate)).toBe(preparer); - }); - - it('should throw an error if the metadata tag does not exist in the entity', () => { - const brokenTemplate: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: {}, - name: 'react-ssr-template', - title: 'React SSR Template', - description: - 'Next.js application skeleton for creating isomorphic web applications.', - uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: '.', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - }, - }; const preparers = new Preparers(); + preparers.register('github.com', preparer); - expect(() => preparers.get(brokenTemplate)).toThrow( - expect.objectContaining({ - message: expect.stringContaining('No location annotation provided'), + expect( + preparers.get('https://github.com/please/find/me/something/from/github'), + ).toBe(preparer); + }); + + it('should throw an error if there is nothing that will match the url provided', async () => { + const preparer = new GithubPreparer( + new ConfigReader({ + host: 'github.com', + apiBaseUrl: 'lols', + token: 'something else yo', }), ); + + const preparers = new Preparers(); + preparers.register('github.com', preparer); + + expect(() => preparers.get('https://404.com')).toThrow( + `Unable to find a preparer for URL: https://404.com. Please make sure to register this host under an integration in app-config`, + ); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index 94d2706cd8..b1d1edf00b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -15,79 +15,74 @@ */ import { Config } from '@backstage/config'; -import { Logger } from 'winston'; import { PreparerBase, PreparerBuilder } from './types'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { - DeprecatedLocationTypeDetector, - makeDeprecatedLocationTypeDetector, - parseLocationAnnotation, -} from '../helpers'; -import { RemoteProtocol } from '../types'; -import { FilePreparer } from './file'; + import { GitlabPreparer } from './gitlab'; import { AzurePreparer } from './azure'; import { GithubPreparer } from './github'; import { BitbucketPreparer } from './bitbucket'; +import { + readAzureIntegrationConfigs, + readBitbucketIntegrationConfigs, + readGitHubIntegrationConfigs, + readGitLabIntegrationConfigs, + ScmIntegrations, +} from '@backstage/integration'; export class Preparers implements PreparerBuilder { - private preparerMap = new Map(); + private preparerMap = new Map(); - constructor(private readonly typeDetector?: DeprecatedLocationTypeDetector) {} - - register(protocol: RemoteProtocol, preparer: PreparerBase) { - this.preparerMap.set(protocol, preparer); + register(host: string, preparer: PreparerBase) { + this.preparerMap.set(host, preparer); } - get(template: TemplateEntityV1alpha1): PreparerBase { - const { protocol, location } = parseLocationAnnotation(template); - - const preparer = this.preparerMap.get(protocol); - + get(url: string): PreparerBase { + const preparer = this.preparerMap.get(new URL(url).host); if (!preparer) { - if ((protocol as string) === 'url') { - const type = this.typeDetector?.(location); - const detected = type && this.preparerMap.get(type as RemoteProtocol); - if (detected) { - return detected; - } - if (type) { - throw new Error( - `No preparer configuration available for type '${type}' with url "${location}". ` + - "Make sure you've added appropriate configuration in the 'scaffolder' configuration section", - ); - } else { - throw new Error( - `Failed to detect preparer type. Unable to determine integration type for location "${location}". ` + - "Please add appropriate configuration to the 'integrations' configuration section", - ); - } - } - throw new Error(`No preparer registered for type: "${protocol}"`); + throw new Error( + `Unable to find a preparer for URL: ${url}. Please make sure to register this host under an integration in app-config`, + ); } - return preparer; } static async fromConfig( config: Config, - { logger }: { logger: Logger }, + // { logger }: { logger: Logger }, ): Promise { - const typeDetector = makeDeprecatedLocationTypeDetector(config); + // TODO(blam/jhaals) + // iterate through the integration configs for each provider, and create the preparer + // register the preparer to hostname. + // if no preparer is returned for the hostname fromConfig, use the backup token credentials from scaffolder block and warn + const preparers = new Preparers(); + const scm = ScmIntegrations.fromConfig(config); + for (const integration of scm.azure.list()) { + preparers.register( + integration.config.host, + AzurePreparer.fromConfig(integration.config), + ); + } - const preparers = new Preparers(typeDetector); + for (const integration of scm.github.list()) { + preparers.register( + integration.config.host, + GithubPreparer.fromConfig(integration.config), + ); + } - const filePreparer = new FilePreparer(); - const gitlabPreparer = GitlabPreparer.fromConfig(config, { logger }); - const azurePreparer = AzurePreparer.fromConfig(config, { logger }); - const githubPreparer = GithubPreparer.fromConfig(config, { logger }); - const bitbucketPreparer = BitbucketPreparer.fromConfig(config, { logger }); + for (const integration of scm.gitlab.list()) { + preparers.register( + integration.config.host, + GitlabPreparer.fromConfig(integration), + ); + } - preparers.register('file', filePreparer); - preparers.register('gitlab', gitlabPreparer); - preparers.register('azure', azurePreparer); - preparers.register('github', githubPreparer); - preparers.register('bitbucket', bitbucketPreparer); + for (const integration of scm.bitbucket.list()) { + preparers.register( + integration.config.host, + BitbucketPreparer.fromConfig(integration.config), + ); + } return preparers; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts index 2974b833a2..8ee78eab3c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts @@ -35,6 +35,6 @@ export interface PreparerBase { } export type PreparerBuilder = { - register(protocol: RemoteProtocol, preparer: PreparerBase): void; - get(template: TemplateEntityV1alpha1): PreparerBase; + register(host: string, preparer: PreparerBase): void; + get(url: string): PreparerBase; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts index 2784a1f9bb..0251b3d983 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts @@ -24,7 +24,6 @@ import { AzurePublisher } from './azure'; import { WebApi } from 'azure-devops-node-api'; import * as helpers from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; describe('Azure Publisher', () => { const logger = getVoidLogger(); @@ -40,23 +39,16 @@ describe('Azure Publisher', () => { ((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi); - const publisher = new AzurePublisher( - new ConfigReader({ - scaffolder: { - azure: { - api: { - baseUrl: 'https://dev.azure.com/myorg', - token: 'fake-azure-token', - }, - }, - }, - }), - ); + const publisher = await AzurePublisher.fromConfig({ + host: 'dev.azure.com', + token: 'fake-azure-token', + }); + mockGitClient.createRepository.mockResolvedValue({ remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', } as { remoteUrl: string }); - const result = await publisher.publish({ + const result = await publisher!.publish({ values: { storePath: 'project/repo', owner: 'bob', @@ -83,59 +75,5 @@ describe('Azure Publisher', () => { logger, }); }); - - it('should use azure-devops-node-api with integrations config', async () => { - const mockGitClient = { - createRepository: jest.fn(), - }; - const mockGitApi = { - getGitApi: jest.fn().mockReturnValue(mockGitClient), - }; - - ((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi); - - const publisher = new AzurePublisher( - new ConfigReader({ - integrations: { - azure: [ - { - host: 'dev.azure.com', - token: 'fake-azure-token', - }, - ], - }, - }), - ); - mockGitClient.createRepository.mockResolvedValue({ - remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - } as { remoteUrl: string }); - - const result = await publisher.publish({ - values: { - storePath: 'https://dev.azure.com/organization/project/_git/repo', - owner: 'bob', - }, - directory: '/tmp/test', - logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - catalogInfoUrl: - 'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml', - }); - expect(mockGitClient.createRepository).toHaveBeenCalledWith( - { - name: 'repo', - }, - 'project', - ); - expect(helpers.initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test', - remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - auth: { username: 'notempty', password: 'fake-azure-token' }, - logger, - }); - }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index d3f900d706..6d2aeea305 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -17,53 +17,35 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { IGitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { Config } from '@backstage/config'; import { initRepoAndPush } from './helpers'; -import { - AzureIntegrationConfig, - readAzureIntegrationConfigs, -} from '@backstage/integration'; +import { AzureIntegrationConfig } from '@backstage/integration'; import gitUrlParse from 'git-url-parse'; import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; export class AzurePublisher implements PublisherBase { - private readonly integrations: AzureIntegrationConfig[]; - private readonly apiBaseUrl?: string; - private readonly token?: string; - - static fromConfig(config: Config) { - return new AzurePublisher(config); + static async fromConfig(config: AzureIntegrationConfig) { + if (!config.token) { + return undefined; + } + const authHandler = getPersonalAccessTokenHandler(config.token); + const webApi = new WebApi(config.host, authHandler); + const azureClient = await webApi.getGitApi(); + return new AzurePublisher(config.token, azureClient); } - constructor(config: Config) { - this.integrations = readAzureIntegrationConfigs( - config.getOptionalConfigArray('integrations.azure') ?? [], - ); + constructor( + private readonly token: string, + private readonly azureClient: IGitApi, + ) {} - this.token = config.getOptionalString('scaffolder.azure.api.token'); - this.apiBaseUrl = config.getOptionalString('scaffolder.azure.api.baseUrl'); - } async publish({ values, directory, logger, }: PublisherOptions): Promise { - const { resource: host, owner, name } = gitUrlParse(values.storePath); + const { owner, name } = gitUrlParse(values.storePath); - const token = this.getToken(host); - if (!token) { - throw new Error('No token provided to create the remote repository'); - } - const baseUrl = this.getBaseUrl(host); - if (!baseUrl) { - throw new Error('No baseUrl provided to create the remote repository'); - } - - const authHandler = getPersonalAccessTokenHandler(token); - const webApi = new WebApi(baseUrl, authHandler); - const azureClient = await webApi.getGitApi(); - - const remoteUrl = await this.createRemote(azureClient, { + const remoteUrl = await this.createRemote({ project: owner, name, }); @@ -75,7 +57,7 @@ export class AzurePublisher implements PublisherBase { remoteUrl, auth: { username: 'notempty', - password: token, + password: this.token, }, logger, }); @@ -83,24 +65,14 @@ export class AzurePublisher implements PublisherBase { return { remoteUrl, catalogInfoUrl }; } - private async createRemote( - client: IGitApi, - opts: { name: string; project: string }, - ) { + private async createRemote(opts: { name: string; project: string }) { const { name, project } = opts; const createOptions: GitRepositoryCreateOptions = { name }; - const repo = await client.createRepository(createOptions, project); + const repo = await this.azureClient.createRepository( + createOptions, + project, + ); return repo.remoteUrl || ''; } - - private getToken(host: string): string | undefined { - return this.token || this.integrations.find(c => c.host === host)?.token; - } - - private getBaseUrl(host: string): string | undefined { - return ( - this.apiBaseUrl || this.integrations.find(c => c.host === host)?.host - ); - } } diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 1ca82d4736..fd051b257c 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -27,6 +27,7 @@ import { StageContext, TemplaterBuilder, PublisherBuilder, + parseLocationAnnotation, } from '../scaffolder'; import { CatalogEntityClient } from '../lib/catalog'; import { validate, ValidatorResult } from 'jsonschema'; @@ -129,7 +130,13 @@ export async function createRouter( { name: 'Prepare the skeleton', handler: async ctx => { - const preparer = preparers.get(ctx.entity); + const { protocol, location: pullPath } = parseLocationAnnotation( + ctx.entity, + ); + + const preparer = + protcol === file ? new FilePreparer() : preparers.get(pullPath); + const skeletonDir = await preparer.prepare(ctx.entity, { logger: ctx.logger, workingDirectory, From 450686ca2734ea2b46bfb26badc0a9596b4a4566 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 15:32:09 +0100 Subject: [PATCH 30/44] Remove duplicated tests --- .../stages/prepare/preparers.test.ts | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts index db6160c276..7023f292f3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts @@ -20,108 +20,6 @@ import { GithubPreparer } from './github'; import { ConfigReader } from '@backstage/config'; describe('Preparers', () => { - // const mockTemplate: TemplateEntityV1alpha1 = { - // apiVersion: 'backstage.io/v1alpha1', - // kind: 'Template', - // metadata: { - // annotations: { - // 'backstage.io/managed-by-location': - // 'file:/Users/bingo/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml', - // }, - // name: 'react-ssr-template', - // title: 'React SSR Template', - // description: - // 'Next.js application skeleton for creating isomorphic web applications.', - // uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - // etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - // generation: 1, - // }, - // spec: { - // templater: 'cookiecutter', - // path: '.', - // type: 'website', - // schema: { - // $schema: 'http://json-schema.org/draft-07/schema#', - // required: ['storePath', 'owner'], - // properties: { - // owner: { - // type: 'string', - // title: 'Owner', - // description: 'Who is going to own this component', - // }, - // storePath: { - // type: 'string', - // title: 'Store path', - // description: 'GitHub store path in org/repo format', - // }, - // }, - // }, - // }, - // }; - // it('should throw an error when the preparer for the source location is not registered', () => { - // const preparers = new Preparers(); - - // expect(() => preparers.get(mockTemplate)).toThrow( - // expect.objectContaining({ - // message: 'No preparer registered for type: "file"', - // }), - // ); - // }); - // it('should return the correct preparer when the source matches', () => { - // const preparers = new Preparers(); - // const preparer = new FilePreparer(); - - // preparers.register('file', preparer); - - // expect(preparers.get(mockTemplate)).toBe(preparer); - // }); - - // it('should throw an error if the metadata tag does not exist in the entity', () => { - // const brokenTemplate: TemplateEntityV1alpha1 = { - // apiVersion: 'backstage.io/v1alpha1', - // kind: 'Template', - // metadata: { - // annotations: {}, - // name: 'react-ssr-template', - // title: 'React SSR Template', - // description: - // 'Next.js application skeleton for creating isomorphic web applications.', - // uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - // etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - // generation: 1, - // }, - // spec: { - // type: 'website', - // templater: 'cookiecutter', - // path: '.', - // schema: { - // $schema: 'http://json-schema.org/draft-07/schema#', - // required: ['storePath', 'owner'], - // properties: { - // owner: { - // type: 'string', - // title: 'Owner', - // description: 'Who is going to own this component', - // }, - // storePath: { - // type: 'string', - // title: 'Store path', - // description: 'GitHub store path in org/repo format', - // }, - // }, - // }, - // }, - // }; - - // const preparers = new Preparers(); - - // expect(() => preparers.get(brokenTemplate)).toThrow( - // expect.objectContaining({ - // message: expect.stringContaining('No location annotation provided'), - // }), - // ); - // }); - it('should return the correct preparer based on the hostname', async () => { const preparer = new GithubPreparer( new ConfigReader({ From ff59698e4e6910739eab7d42150d27bd8d144402 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 15:33:39 +0100 Subject: [PATCH 31/44] Remove old comments --- .../scaffolder/stages/prepare/preparers.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index b1d1edf00b..9a209f4860 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -21,13 +21,7 @@ import { GitlabPreparer } from './gitlab'; import { AzurePreparer } from './azure'; import { GithubPreparer } from './github'; import { BitbucketPreparer } from './bitbucket'; -import { - readAzureIntegrationConfigs, - readBitbucketIntegrationConfigs, - readGitHubIntegrationConfigs, - readGitLabIntegrationConfigs, - ScmIntegrations, -} from '@backstage/integration'; +import { ScmIntegrations } from '@backstage/integration'; export class Preparers implements PreparerBuilder { private preparerMap = new Map(); @@ -46,14 +40,7 @@ export class Preparers implements PreparerBuilder { return preparer; } - static async fromConfig( - config: Config, - // { logger }: { logger: Logger }, - ): Promise { - // TODO(blam/jhaals) - // iterate through the integration configs for each provider, and create the preparer - // register the preparer to hostname. - // if no preparer is returned for the hostname fromConfig, use the backup token credentials from scaffolder block and warn + static async fromConfig(config: Config): Promise { const preparers = new Preparers(); const scm = ScmIntegrations.fromConfig(config); for (const integration of scm.azure.list()) { @@ -73,7 +60,7 @@ export class Preparers implements PreparerBuilder { for (const integration of scm.gitlab.list()) { preparers.register( integration.config.host, - GitlabPreparer.fromConfig(integration), + GitlabPreparer.fromConfig(integration.config), ); } From 8b66c474e30b80135d826af15b2eba38e4126ba8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 16:50:38 +0100 Subject: [PATCH 32/44] Refactor publishers Co-authored-by: blam --- .../stages/publish/bitbucket.test.ts | 35 ++--- .../scaffolder/stages/publish/bitbucket.ts | 124 ++++----------- .../scaffolder/stages/publish/github.test.ts | 94 ++++++------ .../src/scaffolder/stages/publish/github.ts | 90 ++++------- .../scaffolder/stages/publish/gitlab.test.ts | 36 ++--- .../src/scaffolder/stages/publish/gitlab.ts | 86 +++-------- .../stages/publish/publishers.test.ts | 88 +++++++---- .../scaffolder/stages/publish/publishers.ts | 145 ++++++++++++------ .../src/scaffolder/stages/publish/types.ts | 2 +- 9 files changed, 300 insertions(+), 400 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts index c4ef86786e..f75eea939a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts @@ -22,7 +22,6 @@ import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { msw } from '@backstage/test-utils'; -import { ConfigReader } from '@backstage/config'; describe('Bitbucket Publisher', () => { const logger = getVoidLogger(); @@ -59,19 +58,11 @@ describe('Bitbucket Publisher', () => { ), ); - const publisher = new BitbucketPublisher( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - username: 'fake-user', - appPassword: 'fake-token', - }, - ], - }, - }), - ); + const publisher = await BitbucketPublisher.fromConfig({ + host: 'bitbucket.org', + username: 'fake-user', + appPassword: 'fake-token', + }); const result = await publisher.publish({ values: { @@ -126,18 +117,10 @@ describe('Bitbucket Publisher', () => { ), ); - const publisher = new BitbucketPublisher( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.mycompany.com', - token: 'fake-token', - }, - ], - }, - }), - ); + const publisher = await BitbucketPublisher.fromConfig({ + host: 'bitbucket.mycompany.com', + token: 'fake-token', + }); const result = await publisher.publish({ values: { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index ea21f89834..d000eb7ddb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -17,79 +17,50 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; import fetch from 'cross-fetch'; -import { Config } from '@backstage/config'; -import { - BitbucketIntegrationConfig, - readBitbucketIntegrationConfigs, -} from '@backstage/integration'; +import { BitbucketIntegrationConfig } from '@backstage/integration'; import gitUrlParse from 'git-url-parse'; // TODO(blam): We should probably start to use a bitbucket client here that we can change -// the baseURL to point at on-prem or public bitbucket versions like we do for -// github and ghe +// the baseURL to point at on-prem or public bitbucket versions like we do for +// github and ghe. There's to much logic and not enough types here for us to say that this way is better than using +// a supported bitbucket client if one exists. export class BitbucketPublisher implements PublisherBase { - private readonly host?: string; - private readonly username?: string; - private readonly token?: string; - private readonly appPassword?: string; - private readonly integrations: BitbucketIntegrationConfig[]; + static async fromConfig(config: BitbucketIntegrationConfig) { + return new BitbucketPublisher( + config.host, + config.token, + config.appPassword, + config.username, + ); + } - static fromConfig(config: Config) { - return new BitbucketPublisher(config); - } - constructor(config: Config) { - this.integrations = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [], - ); - this.token = config.getOptionalString('scaffolder.bitbucket.api.token'); - this.host = config.getOptionalString('scaffolder.bitbucket.api.host'); - this.username = config.getOptionalString( - 'scaffolder.bitbucket.api.username', - ); - this.appPassword = config.getOptionalString( - 'scaffolder.bitbucket.api.appPassword', - ); - } + constructor( + private readonly host: string, + private readonly token?: string, + private readonly appPassword?: string, + private readonly username?: string, + ) {} async publish({ values, directory, logger, }: PublisherOptions): Promise { - const { resource: hostname, owner: project, name } = gitUrlParse( - values.storePath, - ); - - const token = this.getToken(hostname); - const appPassword = this.getAppPassword(hostname); - const username = this.getUsername(hostname); - - if (!username && !appPassword && !token) { - throw new Error('Cannot create repository without bitbucket credentials'); - } - const host = this.getHost(hostname); - - if (!host) { - throw new Error('No host provided to create the remote repository'); - } + const { owner: project, name } = gitUrlParse(values.storePath); const description = values.description as string; const result = await this.createRemote({ project, name, description, - host, - username, - token, - appPassword, }); await initRepoAndPush({ dir: directory, remoteUrl: result.remoteUrl, auth: { - username: username ? username : 'x-token-auth', - password: appPassword ? appPassword : token ?? '', + username: this.username ? this.username : 'x-token-auth', + password: this.appPassword ? this.appPassword : this.token ?? '', }, logger, }); @@ -97,15 +68,11 @@ export class BitbucketPublisher implements PublisherBase { } private async createRemote(opts: { - username?: string; - token?: string; - appPassword?: string; project: string; name: string; description: string; - host: string; }): Promise { - if (opts.host === 'bitbucket.org') { + if (this.host === 'bitbucket.org') { return this.createBitbucketCloudRepository(opts); } return this.createBitbucketServerRepository(opts); @@ -115,22 +82,11 @@ export class BitbucketPublisher implements PublisherBase { project: string; name: string; description: string; - username?: string; - appPassword?: string; }): Promise { - const { project, name, description, username, appPassword } = opts; - if (!appPassword) { - throw new Error( - 'appPassword is required to create the remote repository', - ); - } - - if (!username) { - throw new Error('username is required to create the remote repository'); - } + const { project, name, description } = opts; let response: Response; - const buffer = Buffer.from(`${username}:${appPassword}`, 'utf8'); + const buffer = Buffer.from(`${this.username}:${this.appPassword}`, 'utf8'); const options: RequestInit = { method: 'POST', @@ -171,13 +127,8 @@ export class BitbucketPublisher implements PublisherBase { project: string; name: string; description: string; - token?: string; - host: string; }): Promise { - const { project, name, description, token, host } = opts; - if (!token) { - throw new Error('No token provided to create the remote repository'); - } + const { project, name, description } = opts; let response: Response; const options: RequestInit = { @@ -187,13 +138,13 @@ export class BitbucketPublisher implements PublisherBase { description: description, }), headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${this.token}`, 'Content-Type': 'application/json', }, }; try { response = await fetch( - `https://${host}/rest/api/1.0/projects/${project}/repos`, + `https://${this.host}/rest/api/1.0/projects/${project}/repos`, options, ); } catch (e) { @@ -212,25 +163,4 @@ export class BitbucketPublisher implements PublisherBase { } throw new Error(`Not a valid response code ${await response.text()}`); } - - private getToken(host: string): string | undefined { - return this.token || this.integrations.find(c => c.host === host)?.token; - } - - private getUsername(host: string): string | undefined { - return ( - this.username || this.integrations.find(c => c.host === host)?.username - ); - } - - private getAppPassword(host: string): string | undefined { - return ( - this.appPassword || - this.integrations.find(c => c.host === host)?.appPassword - ); - } - - private getHost(host: string): string | undefined { - return this.host || this.integrations.find(c => c.host === host)?.host; - } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index c5b8187bc4..29e095a152 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -38,21 +38,16 @@ describe('GitHub Publisher', () => { }); describe('with public repo visibility', () => { - const publisher = new GithubPublisher( - new ConfigReader({ - integrations: { - github: [ - { - token: 'fake-token', - host: 'github.com', - }, - ], - }, - }), - ); - describe('publish: createRemoteInGithub', () => { it('should use octokit to create a repo in an organisation if the organisation property is set', async () => { + const publisher = await GithubPublisher.fromConfig( + { + token: 'fake-token', + host: 'github.com', + }, + { repoVisibility: 'public' }, + ); + mockGithubClient.repos.createInOrg.mockResolvedValue({ data: { clone_url: 'https://github.com/backstage/backstage.git', @@ -64,7 +59,7 @@ describe('GitHub Publisher', () => { }, } as RestEndpointMethodTypes['users']['getByUsername']['response']); - const result = await publisher.publish({ + const result = await publisher!.publish({ values: { storePath: 'https://github.com/blam/test', owner: 'bob', @@ -103,6 +98,14 @@ describe('GitHub Publisher', () => { }); it('should use octokit to create a repo in the authed user if the organisation property is not set', async () => { + const publisher = await GithubPublisher.fromConfig( + { + token: 'fake-token', + host: 'github.com', + }, + { repoVisibility: 'public' }, + ); + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/backstage/backstage.git', @@ -114,7 +117,7 @@ describe('GitHub Publisher', () => { }, } as RestEndpointMethodTypes['users']['getByUsername']['response']); - const result = await publisher.publish({ + const result = await publisher!.publish({ values: { storePath: 'https://github.com/blam/test', owner: 'bob', @@ -147,6 +150,14 @@ describe('GitHub Publisher', () => { }); it('should invite other user in the authed user', async () => { + const publisher = await GithubPublisher.fromConfig( + { + token: 'fake-token', + host: 'github.com', + }, + { repoVisibility: 'public' }, + ); + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/backstage/backstage.git', @@ -158,7 +169,7 @@ describe('GitHub Publisher', () => { }, } as RestEndpointMethodTypes['users']['getByUsername']['response']); - const result = await publisher.publish({ + const result = await publisher!.publish({ values: { storePath: 'https://github.com/blam/test', owner: 'bob', @@ -197,20 +208,15 @@ describe('GitHub Publisher', () => { }); describe('with internal repo visibility', () => { - const publisher = new GithubPublisher( - new ConfigReader({ - integrations: { - github: [{ host: 'github.com', token: 'fake-token' }], - }, - scaffolder: { - github: { - visibility: 'internal', - }, - }, - }), - ); - it('creates a private repository in the organization with visibility set to internal', async () => { + const publisher = await GithubPublisher.fromConfig( + { + token: 'fake-token', + host: 'github.com', + }, + { repoVisibility: 'internal' }, + ); + mockGithubClient.repos.createInOrg.mockResolvedValue({ data: { clone_url: 'https://github.com/backstage/backstage.git', @@ -222,7 +228,7 @@ describe('GitHub Publisher', () => { }, } as RestEndpointMethodTypes['users']['getByUsername']['response']); - const result = await publisher.publish({ + const result = await publisher!.publish({ values: { isOrg: true, storePath: 'https://github.com/blam/test', @@ -253,25 +259,15 @@ describe('GitHub Publisher', () => { }); describe('private visibility in a user account', () => { - const publisher = new GithubPublisher( - new ConfigReader({ - integrations: { - github: [ - { - token: 'fake-token', - host: 'github.com', - }, - ], - }, - scaffolder: { - github: { - visibility: 'private', - }, - }, - }), - ); - it('creates a private repository', async () => { + const publisher = await GithubPublisher.fromConfig( + { + token: 'fake-token', + host: 'github.com', + }, + { repoVisibility: 'private' }, + ); + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/backstage/backstage.git', @@ -283,7 +279,7 @@ describe('GitHub Publisher', () => { }, } as RestEndpointMethodTypes['users']['getByUsername']['response']); - const result = await publisher.publish({ + const result = await publisher!.publish({ values: { storePath: 'https://github.com/blam/test', owner: 'bob', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index a9004966a4..cc8921ce40 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -16,68 +16,54 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; -import { Config } from '@backstage/config'; -import { - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, -} from '@backstage/integration'; +import { GitHubIntegrationConfig } from '@backstage/integration'; import gitUrlParse from 'git-url-parse'; import { Octokit } from '@octokit/rest'; export type RepoVisibilityOptions = 'private' | 'internal' | 'public'; export class GithubPublisher implements PublisherBase { - private scaffolderToken: string | undefined; - private readonly integrations: GitHubIntegrationConfig[]; - private readonly apiBaseUrl: string | undefined; - private readonly repoVisibility: RepoVisibilityOptions; + static async fromConfig( + config: GitHubIntegrationConfig, + { repoVisibility }: { repoVisibility: RepoVisibilityOptions }, + ) { + if (!config.token) { + return undefined; + } - static fromConfig(config: Config) { - return new GithubPublisher(config); - } - constructor(config: Config) { - this.integrations = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - - this.scaffolderToken = config.getOptionalString( - 'scaffolder.github.api.token', - ); - - this.apiBaseUrl = config.getOptionalString('scaffolder.github.api.baseUrl'); - - this.repoVisibility = (config.getOptionalString( - 'scaffolder.github.visibility', - ) ?? 'public') as RepoVisibilityOptions; + const githubClient = new Octokit({ + auth: config.token, + baseUrl: config.apiBaseUrl, + }); + + return new GithubPublisher(config.token, githubClient, repoVisibility); } + constructor( + private readonly token: string, + private readonly client: Octokit, + private readonly repoVisibility: RepoVisibilityOptions, + ) {} async publish({ values, directory, logger, }: PublisherOptions): Promise { - const { resource: host, owner, name } = gitUrlParse(values.storePath); - const token = this.getToken(host); - - if (!token) { - throw new Error('No token provided to create the remote repository'); - } + const { owner, name } = gitUrlParse(values.storePath); const description = values.description as string; const access = values.access as string; const remoteUrl = await this.createRemote({ description, access, - host, name, owner, - token, }); await initRepoAndPush({ dir: directory, remoteUrl, auth: { - username: token ?? '', + username: this.token, password: 'x-oauth-basic', }, logger, @@ -95,30 +81,24 @@ export class GithubPublisher implements PublisherBase { access: string; name: string; owner: string; - host: string; - token: string; description: string; }) { - const { access, description, host, owner, name, token } = opts; + const { access, description, owner, name } = opts; - // create a github client with the config - const githubClient = new Octokit({ - auth: token, - baseUrl: this.getBaseUrl(host), + const user = await this.client.users.getByUsername({ + username: owner, }); - const user = await githubClient.users.getByUsername({ username: owner }); - const repoCreationPromise = user.data.type === 'Organization' - ? githubClient.repos.createInOrg({ + ? this.client.repos.createInOrg({ name, org: owner, private: this.repoVisibility !== 'public', visibility: this.repoVisibility, description, }) - : githubClient.repos.createForAuthenticatedUser({ + : this.client.repos.createForAuthenticatedUser({ name, private: this.repoVisibility === 'private', description, @@ -128,7 +108,7 @@ export class GithubPublisher implements PublisherBase { if (access?.startsWith(`${owner}/`)) { const [, team] = access.split('/'); - await githubClient.teams.addOrUpdateRepoPermissionsInOrg({ + await this.client.teams.addOrUpdateRepoPermissionsInOrg({ org: owner, team_slug: team, owner, @@ -137,7 +117,7 @@ export class GithubPublisher implements PublisherBase { }); // no need to add access if it's the person who own's the personal account } else if (access && access !== owner) { - await githubClient.repos.addCollaborator({ + await this.client.repos.addCollaborator({ owner, repo: name, username: access, @@ -147,18 +127,4 @@ export class GithubPublisher implements PublisherBase { return data?.clone_url; } - - private getToken(host: string): string | undefined { - return ( - this.scaffolderToken || - this.integrations.find(c => c.host === host)?.token - ); - } - - private getBaseUrl(host: string): string | undefined { - return ( - this.apiBaseUrl || - this.integrations.find(c => c.host === host)?.apiBaseUrl - ); - } } 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 fe997f295d..885dfc2fe1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -50,18 +50,10 @@ describe('GitLab Publisher', () => { describe('publish: createRemoteInGitLab', () => { it('should use gitbeaker to create a repo in a namespace if the namespace property is set', async () => { - const publisher = new GitlabPublisher( - new ConfigReader({ - integrations: { - gitlab: [ - { - host: 'gitlab.com', - token: 'fake-token', - }, - ], - }, - }), - ); + const publisher = await GitlabPublisher.fromConfig({ + host: 'gitlab.com', + token: 'fake-token', + }); mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 42, @@ -70,7 +62,7 @@ describe('GitLab Publisher', () => { http_url_to_repo: 'mockclone', } as { http_url_to_repo: string }); - const result = await publisher.publish({ + const result = await publisher!.publish({ values: { isOrg: true, storePath: 'https://gitlab.com/blam/test', @@ -97,18 +89,10 @@ describe('GitLab Publisher', () => { }); it('should use gitbeaker to create a repo in the authed user if the namespace property is not set', async () => { - const publisher = new GitlabPublisher( - new ConfigReader({ - integrations: { - gitlab: [ - { - host: 'gitlab.com', - token: 'fake-token', - }, - ], - }, - }), - ); + const publisher = await GitlabPublisher.fromConfig({ + host: 'gitlab.com', + token: 'fake-token', + }); mockGitlabClient.Namespaces.show.mockResolvedValue({}); mockGitlabClient.Users.current.mockResolvedValue({ @@ -118,7 +102,7 @@ describe('GitLab Publisher', () => { http_url_to_repo: 'mockclone', } as { http_url_to_repo: string }); - const result = await publisher.publish({ + const result = await publisher!.publish({ values: { storePath: 'https://gitlab.com/blam/test', owner: 'bob', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index 76167cf20f..b8139be49e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -16,61 +16,37 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { Gitlab } from '@gitbeaker/node'; -import { Config } from '@backstage/config'; +import { Gitlab as GitlabClient } from '@gitbeaker/core'; import { initRepoAndPush } from './helpers'; import gitUrlParse from 'git-url-parse'; -import { - GitLabIntegrationConfig, - readGitLabIntegrationConfigs, -} from '@backstage/integration'; +import { GitLabIntegrationConfig } from '@backstage/integration'; export class GitlabPublisher implements PublisherBase { - private readonly integrations: GitLabIntegrationConfig[]; - private readonly scaffolderToken: string | undefined; - private readonly apiBaseUrl: string | undefined; + static async fromConfig(config: GitLabIntegrationConfig) { + if (!config.token) { + return undefined; + } - constructor(config: Config) { - this.integrations = readGitLabIntegrationConfigs( - config.getOptionalConfigArray('integrations.gitlab') ?? [], - ); - this.scaffolderToken = config.getOptionalString( - 'scaffolder.gitlab.api.token', - ); - - this.apiBaseUrl = config.getOptionalString('scaffolder.gitlab.api.baseUrl'); + const client = new Gitlab({ host: config.apiBaseUrl, token: config.token }); + return new GitlabPublisher(config.token, client); } - static fromConfig(config: Config) { - return new GitlabPublisher(config); - } + constructor( + private readonly token: string, + private readonly client: GitlabClient, + ) {} async publish({ values, directory, logger, }: PublisherOptions): Promise { - const { resource: host, owner, name } = gitUrlParse(values.storePath); - - const token = this.getToken(host); - if (!token) { - throw new Error( - 'No authentication set for Gitlab publisher. Creating the remote repository is not possible without a token', - ); - } - - const baseUrl = this.getBaseUrl(host); - if (!baseUrl) { - throw new Error( - 'No host set for Gitlab publisher. Creating the remote repository is not possible without a host', - ); - } + const { owner, name } = gitUrlParse(values.storePath); const remoteUrl = await this.createRemote({ - host: baseUrl, owner, name, - token, }); await initRepoAndPush({ @@ -78,7 +54,7 @@ export class GitlabPublisher implements PublisherBase { remoteUrl, auth: { username: 'oauth2', - password: token, + password: this.token, }, logger, }); @@ -90,37 +66,21 @@ export class GitlabPublisher implements PublisherBase { return { remoteUrl, catalogInfoUrl }; } - private getToken(host: string): string | undefined { - return ( - this.scaffolderToken || - this.integrations.find(c => c.host === host)?.token - ); - } + private async createRemote(opts: { name: string; owner: string }) { + const { owner, name } = opts; - private getBaseUrl(host: string): string | undefined { - return ( - this.apiBaseUrl || - this.integrations.find(c => c.host === host)?.apiBaseUrl - ); - } - - private async createRemote(opts: { - host: string; - name: string; - owner: string; - token: string; - }) { - const { owner, name, host, token } = opts; - - const client = new Gitlab({ host: host, token: token }); - let targetNamespace = ((await client.Namespaces.show(owner)) as { + // TODO(blam): this needs cleaning up to be nicer. The amount of brackets is too damn high! + // Shouldn't have to cast things now + let targetNamespace = ((await this.client.Namespaces.show(owner)) as { id: number; }).id; + if (!targetNamespace) { - targetNamespace = ((await client.Users.current()) as { id: number }).id; + targetNamespace = ((await this.client.Users.current()) as { id: number }) + .id; } - const project = (await client.Projects.create({ + const project = (await this.client.Projects.create({ namespace_id: targetNamespace, name: name, })) as { http_url_to_repo: string }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts index 869d1fcd03..87bcd1d003 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts @@ -22,61 +22,86 @@ import { GitlabPublisher } from './gitlab'; import { BitbucketPublisher } from './bitbucket'; jest.mock('@octokit/rest'); +jest.mock('azure-devops-node-api'); describe('Publishers', () => { + const logger = getVoidLogger(); + it('should throw an error when the publisher for the source location is not registered', () => { const publishers = new Publishers(); - expect(() => - publishers.get('https://github.com/org/repo', { - logger: getVoidLogger(), - }), - ).toThrow( + expect(() => publishers.get('https://github.com/org/repo')).toThrow( expect.objectContaining({ message: - 'No matching publisher detected for "https://github.com/org/repo". Please make sure this host is registered in the integration config', + 'Unable to find a publisher for URL: https://github.com/org/repo. Please make sure to register this host under an integration in app-config', }), ); }); it('should return the correct preparer when the source matches for github', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({})); - - expect( - publishers.get('https://github.com/org/repo', { - logger: getVoidLogger(), + const publishers = await Publishers.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'blob' }], + }, }), - ).toBeInstanceOf(GithubPublisher); + { + logger, + }, + ); + + expect(publishers.get('https://github.com/org/repo')).toBeInstanceOf( + GithubPublisher, + ); }); it('should return the correct preparer when the source matches for azure', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({})); + const publishers = await Publishers.fromConfig( + new ConfigReader({ + integrations: { + azure: [{ host: 'dev.azure.com', token: 'blob' }], + }, + }), + { + logger, + }, + ); expect( - publishers.get('https://dev.azure.com/org/project/_git/repo', { - logger: getVoidLogger(), - }), + publishers.get('https://dev.azure.com/org/project/_git/repo'), ).toBeInstanceOf(AzurePublisher); }); it('should return the correct preparer when the source matches for bitbucket', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({})); - - expect( - publishers.get('https://bitbucket.org/owner/repo', { - logger: getVoidLogger(), + const publishers = await Publishers.fromConfig( + new ConfigReader({ + integrations: { + bitbucket: [{ host: 'bitbucket.com', token: 'blob' }], + }, }), - ).toBeInstanceOf(BitbucketPublisher); + { + logger, + }, + ); + expect(publishers.get('https://bitbucket.org/owner/repo')).toBeInstanceOf( + BitbucketPublisher, + ); }); it('should return the correct preparer when the source matches for gitlab', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({})); - - expect( - publishers.get('https://gitlab.com/owner/repo', { - logger: getVoidLogger(), + const publishers = await Publishers.fromConfig( + new ConfigReader({ + integrations: { + gitlab: [{ host: 'gitlab.com', token: 'blob' }], + }, }), - ).toBeInstanceOf(GitlabPublisher); + { + logger, + }, + ); + expect(publishers.get('https://gitlab.com/owner/repo')).toBeInstanceOf( + GitlabPublisher, + ); }); it('should respect registrations for custom URLs for providers using the integrations config', async () => { @@ -88,12 +113,13 @@ describe('Publishers', () => { ], }, }), + { + logger, + }, ); expect( - publishers.get('https://my.special.github.enterprise.thing/org/repo', { - logger: getVoidLogger(), - }), + publishers.get('https://my.special.github.enterprise.thing/org/repo'), ).toBeInstanceOf(GithubPublisher); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 2527ad12b4..d5cbf5c245 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -14,70 +14,125 @@ * limitations under the License. */ -import { Logger } from 'winston'; import { Config } from '@backstage/config'; -import { - DeprecatedLocationTypeDetector, - makeDeprecatedLocationTypeDetector, -} from '../helpers'; import { PublisherBase, PublisherBuilder } from './types'; -import { RemoteProtocol } from '../types'; -import { GithubPublisher } from './github'; +import { GithubPublisher, RepoVisibilityOptions } from './github'; import { GitlabPublisher } from './gitlab'; import { AzurePublisher } from './azure'; import { BitbucketPublisher } from './bitbucket'; +import { Logger } from 'winston'; +import { ScmIntegrations } from '@backstage/integration'; export class Publishers implements PublisherBuilder { - private publisherMap = new Map(); + private publisherMap = new Map(); - constructor(private readonly typeDetector?: DeprecatedLocationTypeDetector) {} - - register(protocol: RemoteProtocol, publisher: PublisherBase) { - this.publisherMap.set(protocol, publisher); + register(host: string, preparer: PublisherBase | undefined) { + this.publisherMap.set(host, preparer); } - get(storePath: string, { logger }: { logger: Logger }): PublisherBase { - const protocol = this.typeDetector?.(storePath); - - if (!protocol) { + get(url: string): PublisherBase { + const preparer = this.publisherMap.get(new URL(url).host); + if (!preparer) { throw new Error( - `No matching publisher detected for "${storePath}". Please make sure this host is registered in the integration config`, + `Unable to find a publisher for URL: ${url}. Please make sure to register this host under an integration in app-config`, ); } - - logger.info( - `Selected publisher ${protocol} for publishing to URL ${storePath}`, - ); - - const publisher = this.publisherMap.get(protocol as RemoteProtocol); - if (!publisher) { - throw new Error( - `Failed to detect publisher type. Unable to determine integration type for location "${protocol}". ` + - "Please add appropriate configuration to the 'integrations' configuration section", - ); - } - - logger.info(`Selected publisher for protocol ${protocol}`); - - return publisher; + return preparer; } - static async fromConfig(config: Config): Promise { - const typeDetector = makeDeprecatedLocationTypeDetector(config); - const publishers = new Publishers(typeDetector); + static async fromConfig( + config: Config, + { logger }: { logger: Logger }, + ): Promise { + const publishers = new Publishers(); - const githubPublisher = GithubPublisher.fromConfig(config); - publishers.register('file', githubPublisher); - publishers.register('github', githubPublisher); + const scm = ScmIntegrations.fromConfig(config); - const gitLabPublisher = GitlabPublisher.fromConfig(config); - publishers.register('gitlab', gitLabPublisher); + for (const integration of scm.azure.list()) { + const publisher = await AzurePublisher.fromConfig(integration.config); - const azurePublisher = AzurePublisher.fromConfig(config); - publishers.register('azure', azurePublisher); + if (publisher) { + publishers.register(integration.config.host, publisher); + } else { + logger.warn('DEPRECATED'); - const bitbucketPublisher = BitbucketPublisher.fromConfig(config); - publishers.register('bitbucket', bitbucketPublisher); + publishers.register( + integration.config.host, + await AzurePublisher.fromConfig({ + token: config.getOptionalString('scaffolder.azure.token'), + host: integration.config.host, + }), + ); + } + } + + for (const integration of scm.github.list()) { + const repoVisibility = (config.getOptionalString( + 'scaffolder.github.visibility', + ) ?? 'public') as RepoVisibilityOptions; + + const publisher = await GithubPublisher.fromConfig(integration.config, { + repoVisibility, + }); + + if (publisher) { + publishers.register(integration.config.host, publisher); + } else { + logger.warn('DEPRECATED'); + + publishers.register( + integration.config.host, + await GithubPublisher.fromConfig( + { + token: config.getOptionalString('scaffolder.github.token') ?? '', + host: integration.config.host, + }, + { repoVisibility }, + ), + ); + } + } + + for (const integration of scm.gitlab.list()) { + const publisher = await GitlabPublisher.fromConfig(integration.config); + + if (publisher) { + publishers.register(integration.config.host, publisher); + } else { + logger.warn('DEPRECATED'); + + publishers.register( + integration.config.host, + await GitlabPublisher.fromConfig({ + token: config.getOptionalString('scaffolder.gitlab.token') ?? '', + host: integration.config.host, + }), + ); + } + } + + for (const integration of scm.bitbucket.list()) { + const publisher = await BitbucketPublisher.fromConfig(integration.config); + + if (publisher) { + publishers.register(integration.config.host, publisher); + } else { + logger.warn('DEPRECATED'); + + publishers.register( + integration.config.host, + await BitbucketPublisher.fromConfig({ + token: config.getOptionalString('scaffolder.bitbucket.token') ?? '', + username: + config.getOptionalString('scaffolder.bitbucket.username') ?? '', + appPassword: + config.getOptionalString('scaffolder.bitbucket.appPassword') ?? + '', + host: integration.config.host, + }), + ); + } + } return publishers; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index cb726dfe09..f135d9a39f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -45,5 +45,5 @@ export type PublisherResult = { export type PublisherBuilder = { register(protocol: RemoteProtocol, publisher: PublisherBase): void; - get(storePath: string, { logger }: { logger: Logger }): PublisherBase; + get(storePath: string): PublisherBase; }; From c0a7910db51cf0a987b08b9569198c863a244273 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 19 Jan 2021 19:04:06 +0100 Subject: [PATCH 33/44] chore: migrate the rest of the scaffolding to the new way for instegration config --- packages/backend/src/plugins/scaffolder.ts | 4 +- .../backend/src/plugins/scaffolder.ts | 5 ++- .../src/scaffolder/stages/helpers.test.ts | 31 +------------ .../src/scaffolder/stages/helpers.ts | 45 +------------------ .../scaffolder/stages/prepare/gitlab.test.ts | 1 - .../stages/prepare/preparers.test.ts | 27 +++++------ .../src/scaffolder/stages/prepare/types.ts | 1 - .../scaffolder/stages/publish/github.test.ts | 1 - .../scaffolder/stages/publish/gitlab.test.ts | 1 - .../src/scaffolder/stages/publish/types.ts | 3 +- .../src/scaffolder/stages/types.ts | 21 --------- .../src/service/router.test.ts | 4 +- .../scaffolder-backend/src/service/router.ts | 13 +++--- 13 files changed, 28 insertions(+), 129 deletions(-) delete mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/types.ts diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 70a961819f..fb1d9babef 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -37,8 +37,8 @@ export default async function createPlugin({ templaters.register('cookiecutter', cookiecutterTemplater); templaters.register('cra', craTemplater); - const preparers = await Preparers.fromConfig(config, { logger }); - const publishers = await Publishers.fromConfig(config); + const preparers = await Preparers.fromConfig(config); + const publishers = await Publishers.fromConfig(config, { logger }); const dockerClient = new Docker(); 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 a0ee16d4ba..590f55c427 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 @@ -18,11 +18,12 @@ export default async function createPlugin({ const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); const templaters = new Templaters(); + templaters.register('cookiecutter', cookiecutterTemplater); templaters.register('cra', craTemplater); - const preparers = await Preparers.fromConfig(config, { logger }); - const publishers = await Publishers.fromConfig(config); + const preparers = await Preparers.fromConfig(config); + const publishers = await Publishers.fromConfig(config, { logger }); const dockerClient = new Docker(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts index d02a0a5ee6..be6d215e5b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts @@ -13,15 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - makeDeprecatedLocationTypeDetector, - parseLocationAnnotation, -} from './helpers'; +import { parseLocationAnnotation } from './helpers'; import { TemplateEntityV1alpha1, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import { ConfigReader } from '@backstage/config'; describe('Helpers', () => { describe('parseLocationAnnotation', () => { @@ -254,29 +250,4 @@ describe('Helpers', () => { }); }); }); - - describe('makeDeprecatedLocationTypeDetector', () => { - it('detects deprecated location types', () => { - const detector = makeDeprecatedLocationTypeDetector( - new ConfigReader({ - integrations: { - github: [{ host: 'derp.com' }, { host: 'foo.com' }], - gitlab: [{ host: 'derp.org' }, { host: 'foo.org' }], - azure: [{ host: 'derp.net' }, { host: 'foo.net' }], - }, - }), - ); - - expect(detector('http://lol:wut@derp.com/wat')).toBe('github'); - expect(detector('https://foo.com/wat')).toBe('github'); - expect(detector('http://derp.org:80/wat')).toBe('gitlab'); - expect(detector('https://foo.org/wat')).toBe('gitlab'); - expect(detector('http://not.derp.net')).toBe(undefined); - expect(detector('http://derp.net')).toBe('azure'); - expect(detector('http://derp.net:8080/wat')).toBe('azure'); - expect(detector('http://github.com')).toBe('github'); - expect(detector('http://gitlab.com')).toBe('gitlab'); - expect(detector('http://dev.azure.com')).toBe('azure'); - }); - }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts index c3ef01b359..f8d8229298 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts @@ -17,12 +17,10 @@ import { TemplateEntityV1alpha1, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; import { InputError } from '@backstage/backend-common'; -import { RemoteProtocol } from './types'; export type ParsedLocationAnnotation = { - protocol: RemoteProtocol; + protocol: 'file' | 'url'; location: string; }; @@ -40,7 +38,7 @@ export const parseLocationAnnotation = ( // split on the first colon for the protocol and the rest after the first split // is the location. const [protocol, location] = annotation.split(/:(.+)/) as [ - RemoteProtocol?, + ('file' | 'url')?, string?, ]; @@ -55,42 +53,3 @@ export const parseLocationAnnotation = ( location, }; }; - -export type DeprecatedLocationTypeDetector = ( - url: string, -) => string | undefined; - -// The reason for the existence of this is to help in migration to using mostly locations -// of type 'url'. This allows us to detect the deprecated location type based on the host, -// which we in turn can use to select out preparer or publisher. -// -// TODO(Rugvip): This should be removed in the future once we fully migrate to using -// integrations configuration for the scaffolder. -export function makeDeprecatedLocationTypeDetector( - config: Config, -): DeprecatedLocationTypeDetector { - const hostMap = new Map(); - - // These are installed by default by the integrations - hostMap.set('github.com', 'github'); - hostMap.set('gitlab.com', 'gitlab'); - hostMap.set('dev.azure.com', 'azure'); - hostMap.set('bitbucket.org', 'bitbucket'); - - config.getOptionalConfigArray('integrations.github')?.forEach(sub => { - hostMap.set(sub.getString('host'), 'github'); - }); - config.getOptionalConfigArray('integrations.gitlab')?.forEach(sub => { - hostMap.set(sub.getString('host'), 'gitlab'); - }); - config.getOptionalConfigArray('integrations.azure')?.forEach(sub => { - hostMap.set(sub.getString('host'), 'azure'); - }); - config.getOptionalConfigArray('integrations.bitbucket')?.forEach(sub => { - hostMap.set(sub.getString('host'), 'bitbucket'); - }); - return (url: string): string | undefined => { - const parsed = new URL(url); - return hostMap.get(parsed.hostname); - }; -} 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 b2f1ac7fd9..a223f72bd5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -24,7 +24,6 @@ import { TemplateEntityV1alpha1, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import { ConfigReader } from '@backstage/config'; import { getVoidLogger, Git } from '@backstage/backend-common'; const mockTemplate = (): TemplateEntityV1alpha1 => ({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts index 7023f292f3..8dd85690cb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts @@ -14,20 +14,15 @@ * limitations under the License. */ import { Preparers } from '.'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { FilePreparer } from './file'; import { GithubPreparer } from './github'; -import { ConfigReader } from '@backstage/config'; describe('Preparers', () => { it('should return the correct preparer based on the hostname', async () => { - const preparer = new GithubPreparer( - new ConfigReader({ - host: 'github.com', - apiBaseUrl: 'lols', - token: 'something else yo', - }), - ); + const preparer = await GithubPreparer.fromConfig({ + host: 'github.com', + apiBaseUrl: 'lols', + token: 'something else yo', + }); const preparers = new Preparers(); preparers.register('github.com', preparer); @@ -38,13 +33,11 @@ describe('Preparers', () => { }); it('should throw an error if there is nothing that will match the url provided', async () => { - const preparer = new GithubPreparer( - new ConfigReader({ - host: 'github.com', - apiBaseUrl: 'lols', - token: 'something else yo', - }), - ); + const preparer = await GithubPreparer.fromConfig({ + host: 'github.com', + apiBaseUrl: 'lols', + token: 'something else yo', + }); const preparers = new Preparers(); preparers.register('github.com', preparer); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts index 8ee78eab3c..17ffea5557 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import type { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { RemoteProtocol } from '../types'; import { Logger } from 'winston'; export type PreparerOptions = { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index 29e095a152..9a788b960d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -18,7 +18,6 @@ jest.mock('@octokit/rest'); jest.mock('./helpers'); import { getVoidLogger } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; import { Octokit, RestEndpointMethodTypes } from '@octokit/rest'; import { GithubPublisher } from './github'; import { initRepoAndPush } from './helpers'; 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 885dfc2fe1..ce207b02c0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -24,7 +24,6 @@ import { GitlabPublisher } from './gitlab'; import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; describe('GitLab Publisher', () => { const logger = getVoidLogger(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index f135d9a39f..456e1c0ca0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -15,7 +15,6 @@ */ import { RequiredTemplateValues } from '../templater'; import { JsonValue } from '@backstage/config'; -import { RemoteProtocol } from '../types'; import { Logger } from 'winston'; /** @@ -44,6 +43,6 @@ export type PublisherResult = { }; export type PublisherBuilder = { - register(protocol: RemoteProtocol, publisher: PublisherBase): void; + register(host: string, publisher: PublisherBase): void; get(storePath: string): PublisherBase; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/types.ts deleted file mode 100644 index 7f12eb71db..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export type RemoteProtocol = - | 'file' - | 'github' - | 'gitlab' - | 'azure' - | 'bitbucket'; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index bbc054e519..3bfd81ae69 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -47,7 +47,7 @@ describe('createRouter - working directory', () => { const mockPreparer = { prepare: mockPrepare, }; - mockPreparers.register('azure', mockPreparer); + mockPreparers.register('dev.azure.com', mockPreparer); }); beforeEach(() => { @@ -65,7 +65,7 @@ describe('createRouter - working directory', () => { kind: 'Template', metadata: { annotations: { - 'backstage.io/managed-by-location': 'azure:dev.azure.com', + 'backstage.io/managed-by-location': 'azure:https://dev.azure.com', }, }, spec: { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index fd051b257c..8af32f6c0c 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -28,6 +28,7 @@ import { TemplaterBuilder, PublisherBuilder, parseLocationAnnotation, + FilePreparer, } from '../scaffolder'; import { CatalogEntityClient } from '../lib/catalog'; import { validate, ValidatorResult } from 'jsonschema'; @@ -135,7 +136,9 @@ export async function createRouter( ); const preparer = - protcol === file ? new FilePreparer() : preparers.get(pullPath); + protocol === 'file' + ? new FilePreparer() + : preparers.get(pullPath); const skeletonDir = await preparer.prepare(ctx.entity, { logger: ctx.logger, @@ -161,9 +164,7 @@ export async function createRouter( { name: 'Publish template', handler: async (ctx: StageContext<{ resultDir: string }>) => { - const publisher = publishers.get(ctx.values.storePath, { - logger: ctx.logger, - }); + const publisher = publishers.get(ctx.values.storePath); ctx.logger.info('Will now store the template'); const result = await publisher.publish({ values: ctx.values, @@ -176,9 +177,9 @@ export async function createRouter( ], }); - res.status(201).json({ id: job.id }); - jobProcessor.run(job); + + res.status(201).json({ id: job.id }); }); const app = express(); From 3d01cca9d54afe7e22ba4f2082bfa81ad8c2d984 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 19 Jan 2021 19:17:58 +0100 Subject: [PATCH 34/44] chore: remove dead code now that's a needless check for the protocol --- .../src/scaffolder/stages/prepare/bitbucket.ts | 10 ++-------- .../src/scaffolder/stages/prepare/github.ts | 9 ++------- .../src/scaffolder/stages/prepare/gitlab.ts | 9 ++------- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 5aafb2730f..8a66176b9c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; -import { InputError, Git } from '@backstage/backend-common'; +import { Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; import { BitbucketIntegrationConfig } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; @@ -42,15 +42,9 @@ export class BitbucketPreparer implements PreparerBase { template: TemplateEntityV1alpha1, opts: PreparerOptions, ): Promise { - const { protocol, location } = parseLocationAnnotation(template); + const { location } = parseLocationAnnotation(template); const workingDirectory = opts.workingDirectory ?? os.tmpdir(); const logger = opts.logger; - - if (!['bitbucket', 'url'].includes(protocol)) { - throw new InputError( - `Wrong location protocol: ${protocol}, should be 'url'`, - ); - } const templateId = template.metadata.name; const repo = parseGitUrl(location); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index e0d669c211..38eb9d408d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; -import { InputError, Git } from '@backstage/backend-common'; +import { Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; import parseGitUrl from 'git-url-parse'; import { GitHubIntegrationConfig } from '@backstage/integration'; @@ -34,15 +34,10 @@ export class GithubPreparer implements PreparerBase { template: TemplateEntityV1alpha1, opts: PreparerOptions, ): Promise { - const { protocol, location } = parseLocationAnnotation(template); + const { location } = parseLocationAnnotation(template); const workingDirectory = opts.workingDirectory ?? os.tmpdir(); const logger = opts.logger; - if (!['github', 'url'].includes(protocol)) { - throw new InputError( - `Wrong location protocol: ${protocol}, should be 'url'`, - ); - } const templateId = template.metadata.name; const parsedGitLocation = parseGitUrl(location); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 0bdecab794..9945a741c6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { InputError, Git } from '@backstage/backend-common'; +import { Git } from '@backstage/backend-common'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { GitLabIntegrationConfig } from '@backstage/integration'; import fs from 'fs-extra'; @@ -34,15 +34,10 @@ export class GitlabPreparer implements PreparerBase { template: TemplateEntityV1alpha1, opts: PreparerOptions, ): Promise { - const { protocol, location } = parseLocationAnnotation(template); + const { location } = parseLocationAnnotation(template); const logger = opts.logger; const workingDirectory = opts.workingDirectory ?? os.tmpdir(); - if (!['gitlab', 'gitlab/api', 'url'].includes(protocol)) { - throw new InputError( - `Wrong location protocol: ${protocol}, should be 'url'`, - ); - } const templateId = template.metadata.name; const parsedGitLocation = parseGitUrl(location); From 49558a35aaad34067da98eafa6e3e207dc74477a Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Jan 2021 09:18:51 +0100 Subject: [PATCH 35/44] Add deprecation warning --- .../src/scaffolder/stages/publish/publishers.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index d5cbf5c245..79eb3b7708 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -48,13 +48,18 @@ export class Publishers implements PublisherBuilder { const scm = ScmIntegrations.fromConfig(config); + const deprecationWarning = (name: string) => { + logger.warn( + `'Specifying credentials for ${name} in the Scaffolder configuration is deprecated. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames'`, + ); + }; + for (const integration of scm.azure.list()) { const publisher = await AzurePublisher.fromConfig(integration.config); - if (publisher) { publishers.register(integration.config.host, publisher); } else { - logger.warn('DEPRECATED'); + deprecationWarning('Azure'); publishers.register( integration.config.host, @@ -74,11 +79,10 @@ export class Publishers implements PublisherBuilder { const publisher = await GithubPublisher.fromConfig(integration.config, { repoVisibility, }); - if (publisher) { publishers.register(integration.config.host, publisher); } else { - logger.warn('DEPRECATED'); + deprecationWarning('GitHub'); publishers.register( integration.config.host, @@ -99,7 +103,7 @@ export class Publishers implements PublisherBuilder { if (publisher) { publishers.register(integration.config.host, publisher); } else { - logger.warn('DEPRECATED'); + deprecationWarning('Gitlab'); publishers.register( integration.config.host, @@ -117,7 +121,7 @@ export class Publishers implements PublisherBuilder { if (publisher) { publishers.register(integration.config.host, publisher); } else { - logger.warn('DEPRECATED'); + deprecationWarning('Bitbucket'); publishers.register( integration.config.host, From 954a7eecabd3f8c28fdf83c434dd9982cb0bdfa7 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Jan 2021 11:13:03 +0100 Subject: [PATCH 36/44] Don't set apiBaseUrl This is already handled by the gitlab client and the default is wrong --- packages/integration/src/gitlab/config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 2d9f4b39ab..1666ece803 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -18,7 +18,6 @@ import { Config } from '@backstage/config'; import { isValidHost } from '../helpers'; const GITLAB_HOST = 'gitlab.com'; -const GITLAB_API_BASE_URL = 'gitlab.com/api/v4'; /** * The configuration parameters for a single GitLab integration. @@ -68,8 +67,6 @@ export function readGitLabIntegrationConfig( if (apiBaseUrl) { apiBaseUrl = apiBaseUrl.replace(/\/+$/, ''); - } else if (host === GITLAB_HOST) { - apiBaseUrl = GITLAB_API_BASE_URL; } return { host, token, apiBaseUrl }; } From ed6baab664f2ce6e651bd118cf6e0f3c0ce7664d Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Jan 2021 11:30:16 +0100 Subject: [PATCH 37/44] Added changeset --- .changeset/rotten-lobsters-grin.md | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .changeset/rotten-lobsters-grin.md diff --git a/.changeset/rotten-lobsters-grin.md b/.changeset/rotten-lobsters-grin.md new file mode 100644 index 0000000000..990feee755 --- /dev/null +++ b/.changeset/rotten-lobsters-grin.md @@ -0,0 +1,52 @@ +--- +'@backstage/create-app': minor +'@backstage/integration': minor +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend': minor +--- + +- Deprecating the `scaffolder.${provider}.token` auth duplication and favoring `integrations.${provider}` instead. If you recieve deprecation warnings your config should change like the following: + +```yaml +scaffolder: + github: + token: + $env: GITHUB_TOKEN + visibility: public +``` + +To something that looks like this: + +```yaml +integration: + github: + - host: github.com + token: + $env: GITHUB_TOKEN +scaffolder: + github: + visibility: public +``` + +You can also configure multiple different hosts under the `integration` config like the following: + +```yaml +integration: + github: + - host: github.com + token: + $env: GITHUB_TOKEN + - host: ghe.mycompany.com + token: + $env: GITHUB_ENTERPRISE_TOKEN +``` + +This of course is the case for all the providers respectively. + +- Adding support for cross provider scaffolding, you can now create repositories in for example Bitbucket using a template residing in GitHub. + +- Fix Gitlab scaffolding so that it returns a `catalogInfoUrl` which automatically imports the project into the catalog. + +- The `Store Path` field on the `scaffolder` frontend has now changed so that you require the full URL to the desired destination repository. + +`backstage/new-repository` would become `https://github.com/backstage/new-repository` if provider was github for example. From 3fb8b9e7cf924cd84941ca02bcbe3ff5dd255ca8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Jan 2021 11:33:56 +0100 Subject: [PATCH 38/44] fix spelling --- .changeset/rotten-lobsters-grin.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/rotten-lobsters-grin.md b/.changeset/rotten-lobsters-grin.md index 990feee755..019ec78733 100644 --- a/.changeset/rotten-lobsters-grin.md +++ b/.changeset/rotten-lobsters-grin.md @@ -5,7 +5,7 @@ '@backstage/plugin-scaffolder-backend': minor --- -- Deprecating the `scaffolder.${provider}.token` auth duplication and favoring `integrations.${provider}` instead. If you recieve deprecation warnings your config should change like the following: +- Deprecating the `scaffolder.${provider}.token` auth duplication and favoring `integrations.${provider}` instead. If you receive deprecation warnings your config should change like the following: ```yaml scaffolder: @@ -45,8 +45,8 @@ This of course is the case for all the providers respectively. - Adding support for cross provider scaffolding, you can now create repositories in for example Bitbucket using a template residing in GitHub. -- Fix Gitlab scaffolding so that it returns a `catalogInfoUrl` which automatically imports the project into the catalog. +- Fix GitLab scaffolding so that it returns a `catalogInfoUrl` which automatically imports the project into the catalog. - The `Store Path` field on the `scaffolder` frontend has now changed so that you require the full URL to the desired destination repository. -`backstage/new-repository` would become `https://github.com/backstage/new-repository` if provider was github for example. +`backstage/new-repository` would become `https://github.com/backstage/new-repository` if provider was GitHub for example. From b470208202627c20cacc83c4d70626ffc876538e Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Jan 2021 11:41:01 +0100 Subject: [PATCH 39/44] Fix config test Co-authored-by: blam --- packages/integration/src/gitlab/config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration/src/gitlab/config.test.ts b/packages/integration/src/gitlab/config.test.ts index e817465b83..bd67202ac7 100644 --- a/packages/integration/src/gitlab/config.test.ts +++ b/packages/integration/src/gitlab/config.test.ts @@ -43,7 +43,7 @@ describe('readGitLabIntegrationConfig', () => { const output = readGitLabIntegrationConfig(buildConfig({})); expect(output).toEqual({ host: 'gitlab.com', - apiBaseUrl: 'gitlab.com/api/v4', + apiBaseUrl: undefined, }); }); From 64826f9e2bf8f16fc9476d84d41293914772fcf2 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Jan 2021 11:52:36 +0100 Subject: [PATCH 40/44] Re-add logger to preparer Co-authored-by: blam --- packages/backend/src/plugins/scaffolder.ts | 3 ++- .../default-app/packages/backend/src/plugins/scaffolder.ts | 4 ++-- .../src/scaffolder/stages/prepare/preparers.ts | 7 ++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index fb1d9babef..4e2257a46c 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -34,10 +34,11 @@ export default async function createPlugin({ const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); const templaters = new Templaters(); + templaters.register('cookiecutter', cookiecutterTemplater); templaters.register('cra', craTemplater); - const preparers = await Preparers.fromConfig(config); + const preparers = await Preparers.fromConfig(config, { logger }); const publishers = await Publishers.fromConfig(config, { logger }); const dockerClient = new Docker(); 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 590f55c427..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 @@ -18,11 +18,11 @@ export default async function createPlugin({ const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); const templaters = new Templaters(); - + templaters.register('cookiecutter', cookiecutterTemplater); templaters.register('cra', craTemplater); - const preparers = await Preparers.fromConfig(config); + const preparers = await Preparers.fromConfig(config, { logger }); const publishers = await Publishers.fromConfig(config, { logger }); const dockerClient = new Docker(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index 9a209f4860..a0b477b66a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -16,6 +16,7 @@ import { Config } from '@backstage/config'; import { PreparerBase, PreparerBuilder } from './types'; +import { Logger } from 'winston'; import { GitlabPreparer } from './gitlab'; import { AzurePreparer } from './azure'; @@ -40,7 +41,11 @@ export class Preparers implements PreparerBuilder { return preparer; } - static async fromConfig(config: Config): Promise { + static async fromConfig( + config: Config, + // eslint-disable-next-line + _: { logger: Logger }, + ): Promise { const preparers = new Preparers(); const scm = ScmIntegrations.fromConfig(config); for (const integration of scm.azure.list()) { From 13231e101b5b07a484e05bea71f4d5585e2c7f86 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 20 Jan 2021 12:47:17 +0100 Subject: [PATCH 41/44] chore: fixing some PR comments --- .../src/github/GithubCredentialsProvider.ts | 4 +-- .../integration/src/gitlab/config.test.ts | 7 +++- packages/integration/src/gitlab/config.ts | 17 +++++++-- .../scaffolder/stages/prepare/gitlab.test.ts | 2 +- .../src/scaffolder/stages/publish/azure.ts | 4 +-- .../scaffolder/stages/publish/bitbucket.ts | 4 +-- .../src/scaffolder/stages/publish/github.ts | 4 +-- .../src/scaffolder/stages/publish/gitlab.ts | 6 ++-- .../src/service/router.test.ts | 2 +- .../components/TemplatePage/TemplatePage.tsx | 36 +++++++++++-------- 10 files changed, 55 insertions(+), 31 deletions(-) diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/GithubCredentialsProvider.ts index 843ef629ab..dfb809ee5f 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import gitUrlParse from 'git-url-parse'; +import parseGitUrl from 'git-url-parse'; import { GithubAppConfig, GitHubIntegrationConfig } from './config'; import { createAppAuth } from '@octokit/auth-app'; import { Octokit, RestEndpointMethodTypes } from '@octokit/rest'; @@ -221,7 +221,7 @@ export class GithubCredentialsProvider { * const { token, headers } = await getCredentials({url: 'github.com/backstage/foobar'}) */ async getCredentials(opts: { url: string }): Promise { - const parsed = gitUrlParse(opts.url); + const parsed = parseGitUrl(opts.url); const owner = parsed.owner || parsed.name; const repo = parsed.owner ? parsed.name : undefined; diff --git a/packages/integration/src/gitlab/config.test.ts b/packages/integration/src/gitlab/config.test.ts index bd67202ac7..7e298e6043 100644 --- a/packages/integration/src/gitlab/config.test.ts +++ b/packages/integration/src/gitlab/config.test.ts @@ -31,11 +31,14 @@ describe('readGitLabIntegrationConfig', () => { buildConfig({ host: 'a.com', token: 't', + baseUrl: 'https://baseurl.for.me/gitlab', }), ); + expect(output).toEqual({ host: 'a.com', token: 't', + baseUrl: 'https://baseurl.for.me/gitlab', }); }); @@ -43,7 +46,8 @@ describe('readGitLabIntegrationConfig', () => { const output = readGitLabIntegrationConfig(buildConfig({})); expect(output).toEqual({ host: 'gitlab.com', - apiBaseUrl: undefined, + apiBaseUrl: 'gitlab.com/api/v4', + baseUrl: 'https://gitlab.com', }); }); @@ -78,6 +82,7 @@ describe('readGitLabIntegrationConfigs', () => { expect(output).toContainEqual({ host: 'a.com', token: 't', + baseUrl: 'https://a.com', }); }); diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 1666ece803..87a402176f 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -18,7 +18,7 @@ import { Config } from '@backstage/config'; import { isValidHost } from '../helpers'; const GITLAB_HOST = 'gitlab.com'; - +const GITLAB_API_BASE_URL = 'gitlab.com/api/v4'; /** * The configuration parameters for a single GitLab integration. */ @@ -29,6 +29,7 @@ export type GitLabIntegrationConfig = { host: string; /** + * @deprecated * The base URL of the API of this provider, e.g. "https://gitlab.com/api/v4", * with no trailing slash. * @@ -45,6 +46,14 @@ export type GitLabIntegrationConfig = { * If no token is specified, anonymous access is used. */ token?: string; + + /** + * The baseUrl of this provider, e.g "https://gitlab.com", + * which is passed into the gitlab client. + * + * If no baseUrl is provided, it will default to https://${host} + */ + baseUrl?: string; }; /** @@ -58,6 +67,7 @@ export function readGitLabIntegrationConfig( const host = config.getOptionalString('host') ?? GITLAB_HOST; let apiBaseUrl = config.getOptionalString('apiBaseUrl'); const token = config.getOptionalString('token'); + const baseUrl = config.getOptionalString('baseUrl') ?? `https://${host}`; if (!isValidHost(host)) { throw new Error( @@ -67,8 +77,11 @@ export function readGitLabIntegrationConfig( if (apiBaseUrl) { apiBaseUrl = apiBaseUrl.replace(/\/+$/, ''); + } else if (host === GITLAB_HOST) { + apiBaseUrl = GITLAB_API_BASE_URL; } - return { host, token, apiBaseUrl }; + + return { host, token, apiBaseUrl, baseUrl }; } /** 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 a223f72bd5..19cbf793e4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -32,7 +32,7 @@ const mockTemplate = (): TemplateEntityV1alpha1 => ({ metadata: { annotations: { [LOCATION_ANNOTATION]: - 'gitlab:https://gitlab.com/benjdlambert/backstage-graphql-template/-/blob/master/template.yaml', + 'url:https://gitlab.com/benjdlambert/backstage-graphql-template/-/blob/master/template.yaml', }, name: 'graphql-starter', title: 'GraphQL Service', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index 6d2aeea305..858809556f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -19,7 +19,7 @@ import { IGitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { initRepoAndPush } from './helpers'; import { AzureIntegrationConfig } from '@backstage/integration'; -import gitUrlParse from 'git-url-parse'; +import parseGitUrl from 'git-url-parse'; import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; export class AzurePublisher implements PublisherBase { @@ -43,7 +43,7 @@ export class AzurePublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const { owner, name } = gitUrlParse(values.storePath); + const { owner, name } = parseGitUrl(values.storePath); const remoteUrl = await this.createRemote({ project: owner, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index d000eb7ddb..ddfc59a6f7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -18,7 +18,7 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; import fetch from 'cross-fetch'; import { BitbucketIntegrationConfig } from '@backstage/integration'; -import gitUrlParse from 'git-url-parse'; +import parseGitUrl from 'git-url-parse'; // TODO(blam): We should probably start to use a bitbucket client here that we can change // the baseURL to point at on-prem or public bitbucket versions like we do for @@ -46,7 +46,7 @@ export class BitbucketPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const { owner: project, name } = gitUrlParse(values.storePath); + const { owner: project, name } = parseGitUrl(values.storePath); const description = values.description as string; const result = await this.createRemote({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index cc8921ce40..01ec7b630a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -17,7 +17,7 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; import { GitHubIntegrationConfig } from '@backstage/integration'; -import gitUrlParse from 'git-url-parse'; +import parseGitUrl from 'git-url-parse'; import { Octokit } from '@octokit/rest'; export type RepoVisibilityOptions = 'private' | 'internal' | 'public'; @@ -48,7 +48,7 @@ export class GithubPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const { owner, name } = gitUrlParse(values.storePath); + const { owner, name } = parseGitUrl(values.storePath); const description = values.description as string; const access = values.access as string; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index b8139be49e..120ca751cd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -18,7 +18,7 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { Gitlab } from '@gitbeaker/node'; import { Gitlab as GitlabClient } from '@gitbeaker/core'; import { initRepoAndPush } from './helpers'; -import gitUrlParse from 'git-url-parse'; +import parseGitUrl from 'git-url-parse'; import { GitLabIntegrationConfig } from '@backstage/integration'; @@ -28,7 +28,7 @@ export class GitlabPublisher implements PublisherBase { return undefined; } - const client = new Gitlab({ host: config.apiBaseUrl, token: config.token }); + const client = new Gitlab({ host: config.baseUrl, token: config.token }); return new GitlabPublisher(config.token, client); } @@ -42,7 +42,7 @@ export class GitlabPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const { owner, name } = gitUrlParse(values.storePath); + const { owner, name } = parseGitUrl(values.storePath); const remoteUrl = await this.createRemote({ owner, diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 3bfd81ae69..785e099c3f 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -65,7 +65,7 @@ describe('createRouter - working directory', () => { kind: 'Template', metadata: { annotations: { - 'backstage.io/managed-by-location': 'azure:https://dev.azure.com', + 'backstage.io/managed-by-location': 'url:https://dev.azure.com', }, }, spec: { diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 11c1029c40..60f5ad8dec 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -39,7 +39,7 @@ import { rootRoute } from '../../routes'; import { JobStatusModal } from '../JobStatusModal'; import { MultistepJsonForm } from '../MultistepJsonForm'; import { useJobPolling } from '../hooks/useJobPolling'; -import gitParse from 'git-url-parse'; +import parseGitUrl from 'git-url-parse'; const useTemplate = ( templateName: string, @@ -180,22 +180,28 @@ export const TemplatePage = () => { schema: OWNER_REPO_SCHEMA, validate: (formData, errors) => { const { storePath } = formData; - const parsedUrl = gitParse(storePath); + try { + const parsedUrl = parseGitUrl(storePath); - if ( - !parsedUrl.resource || - !parsedUrl.owner || - !parsedUrl.name - ) { - if (parsedUrl.resource === 'dev.azure.com') { - errors.storePath.addError( - "The store path should be formatted like https://dev.azure.com/{org}/{project}/_git/{repo} for Azure URL's", - ); - } else { - errors.storePath.addError( - 'The store path should be a complete Git URL to the new repository location. For example: https://github.com/{owner}/{repo}', - ); + if ( + !parsedUrl.resource || + !parsedUrl.owner || + !parsedUrl.name + ) { + if (parsedUrl.resource === 'dev.azure.com') { + errors.storePath.addError( + "The store path should be formatted like https://dev.azure.com/{org}/{project}/_git/{repo} for Azure URL's", + ); + } else { + errors.storePath.addError( + 'The store path should be a complete Git URL to the new repository location. For example: https://github.com/{owner}/{repo}', + ); + } } + } catch (ex) { + errors.storePath.addError( + `Failed validation of the store pathn with message ${ex.message}`, + ); } return errors; From d118c0c07f445b63edc33507007af0608048a620 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 20 Jan 2021 12:57:07 +0100 Subject: [PATCH 42/44] chore(scaffolder-backend/gitlab): added a test to check that the baseUrl is passed through in config --- .../src/scaffolder/stages/publish/gitlab.test.ts | 5 +++++ 1 file changed, 5 insertions(+) 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 ce207b02c0..11b32a6de5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -52,6 +52,7 @@ describe('GitLab Publisher', () => { const publisher = await GitlabPublisher.fromConfig({ host: 'gitlab.com', token: 'fake-token', + baseUrl: 'https://gitlab.hosted.com', }); mockGitlabClient.Namespaces.show.mockResolvedValue({ @@ -71,6 +72,10 @@ describe('GitLab Publisher', () => { logger, }); + expect(Gitlab).toHaveBeenCalledWith({ + token: 'fake-token', + host: 'https://gitlab.hosted.com', + }); expect(result).toEqual({ remoteUrl: 'mockclone', catalogInfoUrl: 'mockclone', From c98d153b73614c8fed52e6ea44fa7ca09353d86b Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Jan 2021 14:25:33 +0100 Subject: [PATCH 43/44] chore: convert constructor args objects Co-authored-by: blam --- .../src/scaffolder/stages/prepare/azure.ts | 8 ++-- .../scaffolder/stages/prepare/bitbucket.ts | 28 +++++++------ .../src/scaffolder/stages/prepare/github.ts | 8 ++-- .../src/scaffolder/stages/prepare/gitlab.ts | 8 ++-- .../src/scaffolder/stages/publish/azure.ts | 11 ++---- .../scaffolder/stages/publish/bitbucket.ts | 39 +++++++++++-------- .../src/scaffolder/stages/publish/github.ts | 32 ++++++++------- .../src/scaffolder/stages/publish/gitlab.ts | 18 +++++---- 8 files changed, 84 insertions(+), 68 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index df127d22e7..8b204fa799 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -25,10 +25,10 @@ import { AzureIntegrationConfig } from '@backstage/integration'; export class AzurePreparer implements PreparerBase { static fromConfig(config: AzureIntegrationConfig) { - return new AzurePreparer(config.token); + return new AzurePreparer({ token: config.token }); } - constructor(private readonly token?: string) {} + constructor(private readonly config: { token?: string }) {} async prepare( template: TemplateEntityV1alpha1, @@ -53,9 +53,9 @@ export class AzurePreparer implements PreparerBase { // Username can be anything but the empty string according to: // https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat - const git = this.token + const git = this.config.token ? Git.fromAuth({ - password: this.token, + password: this.config.token, username: 'notempty', logger, }) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 8a66176b9c..5020a5f658 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -25,17 +25,19 @@ import parseGitUrl from 'git-url-parse'; export class BitbucketPreparer implements PreparerBase { static fromConfig(config: BitbucketIntegrationConfig) { - return new BitbucketPreparer( - config.username, - config.token, - config.appPassword, - ); + return new BitbucketPreparer({ + username: config.username, + token: config.token, + appPassword: config.appPassword, + }); } constructor( - private readonly username?: string, - private readonly token?: string, - private readonly appPassword?: string, + private readonly config: { + username?: string; + token?: string; + appPassword?: string; + }, ) {} async prepare( @@ -78,14 +80,16 @@ export class BitbucketPreparer implements PreparerBase { } private getAuth(): { username: string; password: string } | undefined { - if (this.username && this.appPassword) { - return { username: this.username, password: this.appPassword }; + const { username, token, appPassword } = this.config; + + if (username && appPassword) { + return { username: username, password: appPassword }; } - if (this.token) { + if (token) { return { username: 'x-token-auth', - password: this.token! || this.appPassword!, + password: token! || appPassword!, }; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 38eb9d408d..cfeff454e7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -25,10 +25,10 @@ import { GitHubIntegrationConfig } from '@backstage/integration'; export class GithubPreparer implements PreparerBase { static fromConfig(config: GitHubIntegrationConfig) { - return new GithubPreparer(config.token); + return new GithubPreparer({ token: config.token }); } - constructor(private readonly token?: string) {} + constructor(private readonly config: { token?: string }) {} async prepare( template: TemplateEntityV1alpha1, @@ -53,9 +53,9 @@ export class GithubPreparer implements PreparerBase { const checkoutLocation = path.resolve(tempDir, templateDirectory); - const git = this.token + const git = this.config.token ? Git.fromAuth({ - username: this.token, + username: this.config.token, password: 'x-oauth-basic', logger, }) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 9945a741c6..1fca2ff0f9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -25,10 +25,10 @@ import { PreparerBase, PreparerOptions } from './types'; export class GitlabPreparer implements PreparerBase { static fromConfig(config: GitLabIntegrationConfig) { - return new GitlabPreparer(config.token); + return new GitlabPreparer({ token: config.token }); } - constructor(private readonly token?: string) {} + constructor(private readonly config: { token?: string }) {} async prepare( template: TemplateEntityV1alpha1, @@ -51,9 +51,9 @@ export class GitlabPreparer implements PreparerBase { template.spec.path ?? '.', ); - const git = this.token + const git = this.config.token ? Git.fromAuth({ - password: this.token, + password: this.config.token, username: 'oauth2', logger, }) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index 858809556f..a27eba8a14 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -30,13 +30,10 @@ export class AzurePublisher implements PublisherBase { const authHandler = getPersonalAccessTokenHandler(config.token); const webApi = new WebApi(config.host, authHandler); const azureClient = await webApi.getGitApi(); - return new AzurePublisher(config.token, azureClient); + return new AzurePublisher({ token: config.token, client: azureClient }); } - constructor( - private readonly token: string, - private readonly azureClient: IGitApi, - ) {} + constructor(private readonly config: { token: string; client: IGitApi }) {} async publish({ values, @@ -57,7 +54,7 @@ export class AzurePublisher implements PublisherBase { remoteUrl, auth: { username: 'notempty', - password: this.token, + password: this.config.token, }, logger, }); @@ -68,7 +65,7 @@ export class AzurePublisher implements PublisherBase { private async createRemote(opts: { name: string; project: string }) { const { name, project } = opts; const createOptions: GitRepositoryCreateOptions = { name }; - const repo = await this.azureClient.createRepository( + const repo = await this.config.client.createRepository( createOptions, project, ); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index ddfc59a6f7..a0b867507d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -26,19 +26,21 @@ import parseGitUrl from 'git-url-parse'; // a supported bitbucket client if one exists. export class BitbucketPublisher implements PublisherBase { static async fromConfig(config: BitbucketIntegrationConfig) { - return new BitbucketPublisher( - config.host, - config.token, - config.appPassword, - config.username, - ); + return new BitbucketPublisher({ + host: config.host, + token: config.token, + appPassword: config.appPassword, + username: config.username, + }); } constructor( - private readonly host: string, - private readonly token?: string, - private readonly appPassword?: string, - private readonly username?: string, + private readonly config: { + host: string; + token?: string; + appPassword?: string; + username?: string; + }, ) {} async publish({ @@ -59,8 +61,10 @@ export class BitbucketPublisher implements PublisherBase { dir: directory, remoteUrl: result.remoteUrl, auth: { - username: this.username ? this.username : 'x-token-auth', - password: this.appPassword ? this.appPassword : this.token ?? '', + username: this.config.username ? this.config.username : 'x-token-auth', + password: this.config.appPassword + ? this.config.appPassword + : this.config.token ?? '', }, logger, }); @@ -72,7 +76,7 @@ export class BitbucketPublisher implements PublisherBase { name: string; description: string; }): Promise { - if (this.host === 'bitbucket.org') { + if (this.config.host === 'bitbucket.org') { return this.createBitbucketCloudRepository(opts); } return this.createBitbucketServerRepository(opts); @@ -86,7 +90,10 @@ export class BitbucketPublisher implements PublisherBase { const { project, name, description } = opts; let response: Response; - const buffer = Buffer.from(`${this.username}:${this.appPassword}`, 'utf8'); + const buffer = Buffer.from( + `${this.config.username}:${this.config.appPassword}`, + 'utf8', + ); const options: RequestInit = { method: 'POST', @@ -138,13 +145,13 @@ export class BitbucketPublisher implements PublisherBase { description: description, }), headers: { - Authorization: `Bearer ${this.token}`, + Authorization: `Bearer ${this.config.token}`, 'Content-Type': 'application/json', }, }; try { response = await fetch( - `https://${this.host}/rest/api/1.0/projects/${project}/repos`, + `https://${this.config.host}/rest/api/1.0/projects/${project}/repos`, options, ); } catch (e) { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 01ec7b630a..31d94d5f53 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -35,12 +35,18 @@ export class GithubPublisher implements PublisherBase { baseUrl: config.apiBaseUrl, }); - return new GithubPublisher(config.token, githubClient, repoVisibility); + return new GithubPublisher({ + token: config.token, + client: githubClient, + repoVisibility, + }); } constructor( - private readonly token: string, - private readonly client: Octokit, - private readonly repoVisibility: RepoVisibilityOptions, + private readonly config: { + token: string; + client: Octokit; + repoVisibility: RepoVisibilityOptions; + }, ) {} async publish({ @@ -63,7 +69,7 @@ export class GithubPublisher implements PublisherBase { dir: directory, remoteUrl, auth: { - username: this.token, + username: this.config.token, password: 'x-oauth-basic', }, logger, @@ -85,22 +91,22 @@ export class GithubPublisher implements PublisherBase { }) { const { access, description, owner, name } = opts; - const user = await this.client.users.getByUsername({ + const user = await this.config.client.users.getByUsername({ username: owner, }); const repoCreationPromise = user.data.type === 'Organization' - ? this.client.repos.createInOrg({ + ? this.config.client.repos.createInOrg({ name, org: owner, - private: this.repoVisibility !== 'public', - visibility: this.repoVisibility, + private: this.config.repoVisibility !== 'public', + visibility: this.config.repoVisibility, description, }) - : this.client.repos.createForAuthenticatedUser({ + : this.config.client.repos.createForAuthenticatedUser({ name, - private: this.repoVisibility === 'private', + private: this.config.repoVisibility === 'private', description, }); @@ -108,7 +114,7 @@ export class GithubPublisher implements PublisherBase { if (access?.startsWith(`${owner}/`)) { const [, team] = access.split('/'); - await this.client.teams.addOrUpdateRepoPermissionsInOrg({ + await this.config.client.teams.addOrUpdateRepoPermissionsInOrg({ org: owner, team_slug: team, owner, @@ -117,7 +123,7 @@ export class GithubPublisher implements PublisherBase { }); // no need to add access if it's the person who own's the personal account } else if (access && access !== owner) { - await this.client.repos.addCollaborator({ + await this.config.client.repos.addCollaborator({ owner, repo: name, username: access, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index 120ca751cd..24149ab2f3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -29,12 +29,11 @@ export class GitlabPublisher implements PublisherBase { } const client = new Gitlab({ host: config.baseUrl, token: config.token }); - return new GitlabPublisher(config.token, client); + return new GitlabPublisher({ token: config.token, client }); } constructor( - private readonly token: string, - private readonly client: GitlabClient, + private readonly config: { token: string; client: GitlabClient }, ) {} async publish({ @@ -54,7 +53,7 @@ export class GitlabPublisher implements PublisherBase { remoteUrl, auth: { username: 'oauth2', - password: this.token, + password: this.config.token, }, logger, }); @@ -71,16 +70,19 @@ export class GitlabPublisher implements PublisherBase { // TODO(blam): this needs cleaning up to be nicer. The amount of brackets is too damn high! // Shouldn't have to cast things now - let targetNamespace = ((await this.client.Namespaces.show(owner)) as { + let targetNamespace = ((await this.config.client.Namespaces.show( + owner, + )) as { id: number; }).id; if (!targetNamespace) { - targetNamespace = ((await this.client.Users.current()) as { id: number }) - .id; + targetNamespace = ((await this.config.client.Users.current()) as { + id: number; + }).id; } - const project = (await this.client.Projects.create({ + const project = (await this.config.client.Projects.create({ namespace_id: targetNamespace, name: name, })) as { http_url_to_repo: string }; From 40faae3903f7a629bc6aa6d14c10b1b0797aaf59 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 20 Jan 2021 16:24:23 +0100 Subject: [PATCH 44/44] chore(integration/gitlab): fixing tests again for integration config --- packages/integration/src/gitlab/config.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/integration/src/gitlab/config.test.ts b/packages/integration/src/gitlab/config.test.ts index eb3ccd643f..303b4bd270 100644 --- a/packages/integration/src/gitlab/config.test.ts +++ b/packages/integration/src/gitlab/config.test.ts @@ -46,7 +46,7 @@ describe('readGitLabIntegrationConfig', () => { const output = readGitLabIntegrationConfig(buildConfig({})); expect(output).toEqual({ host: 'gitlab.com', - apiBaseUrl: 'gitlab.com/api/v4', + apiBaseUrl: 'https://gitlab.com/api/v4', baseUrl: 'https://gitlab.com', }); }); @@ -58,6 +58,7 @@ describe('readGitLabIntegrationConfig', () => { expect(output).toEqual({ host: 'gitlab.com', + baseUrl: 'https://gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', }); });