Fix prepare, e2e tested with GitHub

This commit is contained in:
Johan Haals
2021-01-15 14:34:21 +01:00
parent 2249b22e3e
commit 548dbf154e
9 changed files with 131 additions and 93 deletions
@@ -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<string> {
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,
@@ -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<string> {
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,
@@ -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<string> {
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,
@@ -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<string> {
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,
@@ -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 {
@@ -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<PublisherResult> {
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<string, JsonValue>,
) {
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
);
}
}
@@ -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) {
@@ -36,6 +36,7 @@ export type PublisherBase = {
export type PublisherOptions = {
values: RequiredTemplateValues & Record<string, JsonValue>;
directory: string;
logger: Logger;
};
export type PublisherResult = {
@@ -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',
);
}