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', ); }