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; };