Add initial support for Bitbucket Server scaffolding
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user