From 544139ed16c31a0791428a9d33d5b4b596c9616a Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 15:31:07 +0100 Subject: [PATCH] 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,