Add initial support for Bitbucket Server scaffolding

This commit is contained in:
Mathias Åhsberg
2020-12-10 12:32:47 +00:00
parent fb745571bc
commit d9eba5b715
6 changed files with 226 additions and 2 deletions
@@ -85,7 +85,9 @@ export function makeDeprecatedLocationTypeDetector(
config.getOptionalConfigArray('integrations.azure')?.forEach(sub => {
hostMap.set(sub.getString('host'), 'azure/api');
});
config.getOptionalConfigArray('integrations.bitbucket')?.forEach(sub => {
hostMap.set(sub.getString('host'), 'bitbucket/api');
});
return (url: string): string | undefined => {
const parsed = new URL(url);
return hostMap.get(parsed.hostname);
@@ -0,0 +1,85 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import os from 'os';
import fs from 'fs-extra';
import path from 'path';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from '../helpers';
import { InputError } from '@backstage/backend-common';
import { PreparerBase, PreparerOptions } from './types';
import GitUriParser from 'git-url-parse';
import { Clone, Cred } from 'nodegit';
import { Config } from '@backstage/config';
export class BitbucketPreparer implements PreparerBase {
private readonly privateToken: string;
private readonly user: string;
constructor(config: Config) {
this.user =
config.getOptionalString('scaffolder.bitbucket.api.username') ?? '';
this.privateToken =
config.getOptionalString('scaffolder.bitbucket.api.token') ?? '';
}
async prepare(
template: TemplateEntityV1alpha1,
opts: PreparerOptions,
): Promise<string> {
const { protocol, location } = parseLocationAnnotation(template);
const workingDirectory = opts?.workingDirectory ?? os.tmpdir();
if (!['bitbucket/api', 'url'].includes(protocol)) {
throw new InputError(
`Wrong location protocol: ${protocol}, should be 'url'`,
);
}
const templateId = template.metadata.name;
const repo = GitUriParser(location);
let repositoryCheckoutUrl;
// could be refactor once https://github.com/IonicaBizau/git-url-parse/pull/117 has been published
if (repo.source === 'bitbucket.org') {
repositoryCheckoutUrl = `${repo.protocol}://${repo.resource}/${repo.owner}/${repo.name}`;
} else {
repositoryCheckoutUrl = `${repo.protocol}://${repo.resource}/scm/${repo.owner}/${repo.name}`;
}
const tempDir = await fs.promises.mkdtemp(
path.join(workingDirectory, templateId),
);
const templateDirectory = path.join(
`${path.dirname(repo.filepath)}`,
template.spec.path ?? '.',
);
const options = this.privateToken
? {
fetchOpts: {
callbacks: {
credentials: () =>
Cred.userpassPlaintextNew(this.user, this.privateToken),
},
},
}
: {};
await Clone.clone(repositoryCheckoutUrl, tempDir, options);
return path.resolve(tempDir, templateDirectory);
}
}
@@ -28,6 +28,7 @@ import { FilePreparer } from './file';
import { GitlabPreparer } from './gitlab';
import { AzurePreparer } from './azure';
import { GithubPreparer } from './github';
import { BitbucketPreparer } from './bitbucket';
export class Preparers implements PreparerBuilder {
private preparerMap = new Map<RemoteProtocol, PreparerBase>();
@@ -79,11 +80,13 @@ export class Preparers implements PreparerBuilder {
const filePreparer = new FilePreparer();
const gitlabPreparer = new GitlabPreparer(config);
const azurePreparer = new AzurePreparer(config);
const bitbucketPreparer = new BitbucketPreparer(config);
preparers.register('file', filePreparer);
preparers.register('gitlab', gitlabPreparer);
preparers.register('gitlab/api', gitlabPreparer);
preparers.register('azure/api', azurePreparer);
preparers.register('bitbucket/api', bitbucketPreparer);
const githubConfig = config.getOptionalConfig('scaffolder.github');
if (githubConfig) {
@@ -0,0 +1,104 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
import { pushToRemoteUserPass } from './helpers';
import { RequiredTemplateValues } from '../templater';
import { JsonValue } from '../../../../../../packages/config/src';
import fetch from 'cross-fetch';
export class BitbucketPublisher implements PublisherBase {
private readonly host: string;
private readonly username: string;
private readonly token: string;
constructor(host: string, username: string, token: string) {
this.host = host;
this.username = username;
this.token = token;
}
async publish({
values,
directory,
}: PublisherOptions): Promise<PublisherResult> {
const result = await this.createRemote(values);
await pushToRemoteUserPass(
directory,
result.remoteUrl,
this.username,
this.token,
);
return result;
}
private async createRemote(
values: RequiredTemplateValues & Record<string, JsonValue>,
): Promise<PublisherResult> {
const [project, name] = values.storePath.split('/');
if (this.host === 'bitbucket.org') {
return this.createBitbucketCloudRepository(project, name);
}
return this.createBitbucketServerRepository(project, name);
}
private async createBitbucketCloudRepository(
project: string,
name: string,
): Promise<PublisherResult> {
throw new Error(
`Failed to create ${project}/${name} on bitbucket.org which is currently not supported`,
);
}
private async createBitbucketServerRepository(
project: string,
name: string,
): Promise<PublisherResult> {
let response: Response;
const options: RequestInit = {
method: 'POST',
body: JSON.stringify({
name: name,
}),
headers: {
Authorization: `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
};
try {
response = await fetch(
`${this.host}/rest/api/1.0/projects/${project}/repos`,
options,
);
} catch (e) {
throw new Error(`Unable to create repository, ${e}`);
}
if (response.status === 201) {
const r = await response.json();
let remoteUrl = '';
for (const link of r.links.clone) {
if (link.name === 'http') {
remoteUrl = link.href;
}
}
const catalogInfoUrl = `${r.links.self[0].href}/catalog-info.yaml`;
return { remoteUrl, catalogInfoUrl };
}
throw new Error(`Not a valid response code ${await response.text()}`);
}
}
@@ -30,6 +30,7 @@ import { RemoteProtocol } from '../types';
import { GithubPublisher, RepoVisibilityOptions } from './github';
import { GitlabPublisher } from './gitlab';
import { AzurePublisher } from './azure';
import { BitbucketPublisher } from './bitbucket';
export class Publishers implements PublisherBuilder {
private publisherMap = new Map<RemoteProtocol, PublisherBase>();
@@ -163,6 +164,34 @@ export class Publishers implements PublisherBuilder {
}
}
const bitbucketConfig = config.getOptionalConfig(
'scaffolder.bitbucket.api',
);
if (bitbucketConfig) {
try {
const baseUrl = bitbucketConfig.getString('host');
const bitbucketUsername = bitbucketConfig.getString('username');
const bitbucketToken = bitbucketConfig.getString('token');
const bitbucketPublisher = new BitbucketPublisher(
baseUrl,
bitbucketUsername,
bitbucketToken,
);
publishers.register('bitbucket/api', bitbucketPublisher);
} catch (e) {
const providerName = 'bitbucket';
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
);
}
logger.warn(
`Skipping ${providerName} scaffolding provider, ${e.message}`,
);
}
}
return publishers;
}
}
@@ -18,4 +18,5 @@ export type RemoteProtocol =
| 'github'
| 'gitlab'
| 'gitlab/api'
| 'azure/api';
| 'azure/api'
| 'bitbucket/api';