Refactor preparers
This commit is contained in:
@@ -26,7 +26,6 @@ import {
|
||||
LOCATION_ANNOTATION,
|
||||
} from '@backstage/catalog-model';
|
||||
import { getVoidLogger, Git } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('AzurePreparer', () => {
|
||||
const mockGitClient = {
|
||||
@@ -80,20 +79,13 @@ describe('AzurePreparer', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const preparer = AzurePreparer.fromConfig({
|
||||
host: 'dev.azure.com',
|
||||
token: 'fake-azure-token',
|
||||
});
|
||||
|
||||
// TODO(blam): Here's a test that will fail when the deprecation is complete
|
||||
it('calls the clone command with deprecated token', async () => {
|
||||
const preparer = new AzurePreparer(
|
||||
new ConfigReader({
|
||||
scaffolder: {
|
||||
azure: {
|
||||
api: {
|
||||
token: 'fake-azure-token',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
@@ -104,28 +96,16 @@ describe('AzurePreparer', () => {
|
||||
});
|
||||
|
||||
it('calls the clone command with token from integrations config', async () => {
|
||||
const preparer = new AzurePreparer(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
azure: [
|
||||
{ host: 'dev.azure.com', token: 'fake-azure-token-integration' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
logger,
|
||||
password: 'fake-azure-token-integration',
|
||||
password: 'fake-azure-token',
|
||||
username: 'notempty',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the clone command with the correct arguments for a repository', async () => {
|
||||
const preparer = new AzurePreparer(new ConfigReader({}));
|
||||
|
||||
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
|
||||
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
@@ -136,7 +116,6 @@ describe('AzurePreparer', () => {
|
||||
});
|
||||
|
||||
it('calls the clone command with the correct arguments for a repository when no path is provided', async () => {
|
||||
const preparer = new AzurePreparer(new ConfigReader({}));
|
||||
delete mockEntity.spec.path;
|
||||
|
||||
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
|
||||
@@ -149,7 +128,6 @@ describe('AzurePreparer', () => {
|
||||
});
|
||||
|
||||
it('return the temp directory with the path to the folder if it is specified', async () => {
|
||||
const preparer = new AzurePreparer(new ConfigReader({}));
|
||||
mockEntity.spec.path = './template/test/1/2/3';
|
||||
|
||||
const response = await preparer.prepare(mockEntity, {
|
||||
@@ -162,7 +140,6 @@ describe('AzurePreparer', () => {
|
||||
});
|
||||
|
||||
it('return the working directory with the path to the folder if it is specified', async () => {
|
||||
const preparer = new AzurePreparer(new ConfigReader({}));
|
||||
mockEntity.spec.path = './template/test/1/2/3';
|
||||
|
||||
const response = await preparer.prepare(mockEntity, {
|
||||
|
||||
@@ -18,54 +18,26 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { parseLocationAnnotation } from '../helpers';
|
||||
import { InputError, Git } from '@backstage/backend-common';
|
||||
import { Git } from '@backstage/backend-common';
|
||||
import { PreparerBase, PreparerOptions } from './types';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
readAzureIntegrationConfigs,
|
||||
AzureIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import { AzureIntegrationConfig } from '@backstage/integration';
|
||||
|
||||
export class AzurePreparer implements PreparerBase {
|
||||
private readonly integrations: AzureIntegrationConfig[];
|
||||
private readonly scaffolderToken: string | undefined;
|
||||
|
||||
static fromConfig(config: Config, { logger }: { logger: Logger }) {
|
||||
if (config.getOptionalString('scaffolder.azure.api.token')) {
|
||||
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",
|
||||
'Please migrate to using integrations config and specifying tokens under hostnames',
|
||||
);
|
||||
}
|
||||
|
||||
return new AzurePreparer(config);
|
||||
static fromConfig(config: AzureIntegrationConfig) {
|
||||
return new AzurePreparer(config.token);
|
||||
}
|
||||
|
||||
constructor(config: Config) {
|
||||
this.integrations = readAzureIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.azure') ?? [],
|
||||
);
|
||||
|
||||
this.scaffolderToken = config.getOptionalString(
|
||||
'scaffolder.azure.api.token',
|
||||
);
|
||||
}
|
||||
constructor(private readonly token?: string) {}
|
||||
|
||||
async prepare(
|
||||
template: TemplateEntityV1alpha1,
|
||||
opts: PreparerOptions,
|
||||
): Promise<string> {
|
||||
const { protocol, location } = parseLocationAnnotation(template);
|
||||
const { location } = parseLocationAnnotation(template);
|
||||
const workingDirectory = opts.workingDirectory ?? os.tmpdir();
|
||||
const logger = opts.logger;
|
||||
|
||||
if (!['azure', 'url'].includes(protocol)) {
|
||||
throw new InputError(
|
||||
`Wrong location protocol: ${protocol}, should be 'url'`,
|
||||
);
|
||||
}
|
||||
const templateId = template.metadata.name;
|
||||
|
||||
const parsedGitLocation = parseGitUrl(location);
|
||||
@@ -79,13 +51,11 @@ export class AzurePreparer implements PreparerBase {
|
||||
template.spec.path ?? '.',
|
||||
);
|
||||
|
||||
const token = this.getToken(parsedGitLocation.resource);
|
||||
|
||||
// Username can be anything but the empty string according to:
|
||||
// https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat
|
||||
const git = token
|
||||
const git = this.token
|
||||
? Git.fromAuth({
|
||||
password: token,
|
||||
password: this.token,
|
||||
username: 'notempty',
|
||||
logger,
|
||||
})
|
||||
@@ -98,11 +68,4 @@ export class AzurePreparer implements PreparerBase {
|
||||
|
||||
return path.resolve(tempDir, templateDirectory);
|
||||
}
|
||||
|
||||
private getToken(host: string): string | undefined {
|
||||
return (
|
||||
this.scaffolderToken ||
|
||||
this.integrations.find(c => c.host === host)?.token
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
LOCATION_ANNOTATION,
|
||||
} from '@backstage/catalog-model';
|
||||
import { getVoidLogger, Git } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('BitbucketPreparer', () => {
|
||||
let mockEntity: TemplateEntityV1alpha1;
|
||||
@@ -79,8 +78,13 @@ describe('BitbucketPreparer', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const preparer = BitbucketPreparer.fromConfig({
|
||||
host: 'bitbucket.org',
|
||||
username: 'fake-user',
|
||||
appPassword: 'fake-password',
|
||||
});
|
||||
|
||||
it('calls the clone command with the correct arguments for a repository', async () => {
|
||||
const preparer = new BitbucketPreparer(new ConfigReader({}));
|
||||
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
url: 'https://bitbucket.org/backstage-project/backstage-repo',
|
||||
@@ -89,20 +93,11 @@ describe('BitbucketPreparer', () => {
|
||||
});
|
||||
|
||||
it('calls the clone command with the correct arguments if an app password is provided for a repository', async () => {
|
||||
const preparer = new BitbucketPreparer(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
username: 'fake-user',
|
||||
appPassword: 'fake-password',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const preparer = BitbucketPreparer.fromConfig({
|
||||
host: 'bitbucket.org',
|
||||
username: 'fake-user',
|
||||
appPassword: 'fake-password',
|
||||
});
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
@@ -113,7 +108,6 @@ describe('BitbucketPreparer', () => {
|
||||
});
|
||||
|
||||
it('calls the clone command with the correct arguments for a repository when no path is provided', async () => {
|
||||
const preparer = new BitbucketPreparer(new ConfigReader({}));
|
||||
delete mockEntity.spec.path;
|
||||
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
@@ -123,7 +117,6 @@ describe('BitbucketPreparer', () => {
|
||||
});
|
||||
|
||||
it('return the temp directory with the path to the folder if it is specified', async () => {
|
||||
const preparer = new BitbucketPreparer(new ConfigReader({}));
|
||||
mockEntity.spec.path = './template/test/1/2/3';
|
||||
const response = await preparer.prepare(mockEntity, {
|
||||
logger: getVoidLogger(),
|
||||
@@ -134,79 +127,22 @@ describe('BitbucketPreparer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('calls the clone command with deprecated auth method', async () => {
|
||||
const preparer = new BitbucketPreparer(
|
||||
new ConfigReader({
|
||||
scaffolder: {
|
||||
bitbucket: {
|
||||
api: {
|
||||
username: 'fakeusername',
|
||||
token: 'faketoken',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
logger,
|
||||
username: 'fakeusername',
|
||||
password: 'faketoken',
|
||||
it('calls the clone command with with token for auth method', async () => {
|
||||
const preparer = BitbucketPreparer.fromConfig({
|
||||
host: 'bitbucket.org',
|
||||
token: 'fake-token',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the clone command with integrations config for auth method', async () => {
|
||||
const preparer = new BitbucketPreparer(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
username: 'asd3',
|
||||
token: 'faketoken',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
logger,
|
||||
username: 'asd3',
|
||||
password: 'faketoken',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the clone command with integrations config with appPassword for auth method', async () => {
|
||||
const preparer = new BitbucketPreparer(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
username: 'asd3',
|
||||
appPassword: 'myapppassword',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
logger,
|
||||
username: 'asd3',
|
||||
password: 'myapppassword',
|
||||
username: 'x-token-auth',
|
||||
password: 'fake-token',
|
||||
});
|
||||
});
|
||||
|
||||
it('return the working directory with the path to the folder if it is specified', async () => {
|
||||
const preparer = new BitbucketPreparer(new ConfigReader({}));
|
||||
mockEntity.spec.path = './template/test/1/2/3';
|
||||
const response = await preparer.prepare(mockEntity, {
|
||||
workingDirectory: '/workDir',
|
||||
|
||||
@@ -20,44 +20,23 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { parseLocationAnnotation } from '../helpers';
|
||||
import { InputError, Git } from '@backstage/backend-common';
|
||||
import { PreparerBase, PreparerOptions } from './types';
|
||||
import {
|
||||
readBitbucketIntegrationConfigs,
|
||||
BitbucketIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import { BitbucketIntegrationConfig } from '@backstage/integration';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export class BitbucketPreparer implements PreparerBase {
|
||||
private readonly privateToken: string;
|
||||
private readonly username: string;
|
||||
private readonly integrations: BitbucketIntegrationConfig[];
|
||||
|
||||
static fromConfig(config: Config, { logger }: { logger: Logger }) {
|
||||
const user = config.getOptionalString('scaffolder.bitbucket.api.username');
|
||||
const token = config.getOptionalString('scaffolder.bitbucket.api.token');
|
||||
const password = config.getOptionalString(
|
||||
'scaffolder.bitbucket.api.appPassword',
|
||||
static fromConfig(config: BitbucketIntegrationConfig) {
|
||||
return new BitbucketPreparer(
|
||||
config.username,
|
||||
config.token,
|
||||
config.appPassword,
|
||||
);
|
||||
|
||||
if (user || token || password) {
|
||||
logger.warn(
|
||||
"DEPRECATION: Setting credentials under 'scaffolder.bitbucket.api' will not be respected in future releases. Please consider using integrations config instead",
|
||||
'Please migrate to using integrations config and specifying tokens under hostnames',
|
||||
);
|
||||
}
|
||||
return new BitbucketPreparer(config);
|
||||
}
|
||||
constructor(config: Config) {
|
||||
this.integrations = readBitbucketIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.bitbucket') ?? [],
|
||||
);
|
||||
|
||||
this.username =
|
||||
config.getOptionalString('scaffolder.bitbucket.api.username') ?? '';
|
||||
this.privateToken =
|
||||
config.getOptionalString('scaffolder.bitbucket.api.token') ?? '';
|
||||
}
|
||||
constructor(
|
||||
private readonly username?: string,
|
||||
private readonly token?: string,
|
||||
private readonly appPassword?: string,
|
||||
) {}
|
||||
|
||||
async prepare(
|
||||
template: TemplateEntityV1alpha1,
|
||||
@@ -88,8 +67,7 @@ export class BitbucketPreparer implements PreparerBase {
|
||||
|
||||
const checkoutLocation = path.resolve(tempDir, templateDirectory);
|
||||
|
||||
const auth = this.getAuth(repo.resource);
|
||||
|
||||
const auth = this.getAuth();
|
||||
const git = auth
|
||||
? Git.fromAuth({
|
||||
...auth,
|
||||
@@ -105,36 +83,18 @@ export class BitbucketPreparer implements PreparerBase {
|
||||
return checkoutLocation;
|
||||
}
|
||||
|
||||
private getAuth(
|
||||
host: string,
|
||||
): { username: string; password: string } | undefined {
|
||||
if (this.username && this.privateToken) {
|
||||
return { username: this.username, password: this.privateToken };
|
||||
private getAuth(): { username: string; password: string } | undefined {
|
||||
if (this.username && this.appPassword) {
|
||||
return { username: this.username, password: this.appPassword };
|
||||
}
|
||||
|
||||
const bitbucketIntegrationConfig = this.integrations.find(
|
||||
c => c.host === host,
|
||||
);
|
||||
|
||||
if (!bitbucketIntegrationConfig) {
|
||||
return undefined;
|
||||
if (this.token) {
|
||||
return {
|
||||
username: 'x-token-auth',
|
||||
password: this.token! || this.appPassword!,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
!bitbucketIntegrationConfig.username ||
|
||||
!(
|
||||
bitbucketIntegrationConfig.token ||
|
||||
bitbucketIntegrationConfig.appPassword
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
username: bitbucketIntegrationConfig.username,
|
||||
password:
|
||||
bitbucketIntegrationConfig.token! ||
|
||||
bitbucketIntegrationConfig.appPassword!,
|
||||
};
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
LOCATION_ANNOTATION,
|
||||
} from '@backstage/catalog-model';
|
||||
import { getVoidLogger, Git } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('GitHubPreparer', () => {
|
||||
let mockEntity: TemplateEntityV1alpha1;
|
||||
@@ -78,17 +77,13 @@ describe('GitHubPreparer', () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
it('calls the clone command with the correct arguments for a repository', async () => {
|
||||
const preparer = new GithubPreparer(
|
||||
new ConfigReader({
|
||||
scaffolder: {
|
||||
github: {
|
||||
token: 'fake-token',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const preparer = GithubPreparer.fromConfig({
|
||||
host: 'github.com',
|
||||
token: 'fake-token',
|
||||
});
|
||||
|
||||
it('calls the clone command with the correct arguments for a repository', async () => {
|
||||
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
|
||||
|
||||
expect(mockGitClient.clone).toHaveBeenCalledWith({
|
||||
@@ -96,8 +91,8 @@ describe('GitHubPreparer', () => {
|
||||
dir: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the clone command with the correct arguments for a repository when no path is provided', async () => {
|
||||
const preparer = new GithubPreparer(new ConfigReader({}));
|
||||
delete mockEntity.spec.path;
|
||||
|
||||
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
|
||||
@@ -109,7 +104,10 @@ describe('GitHubPreparer', () => {
|
||||
});
|
||||
|
||||
it('return the temp directory with the path to the folder if it is specified', async () => {
|
||||
const preparer = new GithubPreparer(new ConfigReader({}));
|
||||
const preparer = GithubPreparer.fromConfig({
|
||||
host: 'github.com',
|
||||
token: 'fake-token',
|
||||
});
|
||||
mockEntity.spec.path = './template/test/1/2/3';
|
||||
const response = await preparer.prepare(mockEntity, {
|
||||
logger: getVoidLogger(),
|
||||
@@ -120,7 +118,11 @@ describe('GitHubPreparer', () => {
|
||||
});
|
||||
|
||||
it('return the working directory with the path to the folder if it is specified', async () => {
|
||||
const preparer = new GithubPreparer(new ConfigReader({}));
|
||||
const preparer = GithubPreparer.fromConfig({
|
||||
host: 'github.com',
|
||||
token: 'fake-token',
|
||||
});
|
||||
|
||||
mockEntity.spec.path = './template/test/1/2/3';
|
||||
const response = await preparer.prepare(mockEntity, {
|
||||
workingDirectory: '/workDir',
|
||||
@@ -132,17 +134,7 @@ describe('GitHubPreparer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('calls the clone command with deprecated token', async () => {
|
||||
const preparer = new GithubPreparer(
|
||||
new ConfigReader({
|
||||
scaffolder: {
|
||||
github: {
|
||||
token: 'fake-token',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
it('calls the clone command with token', async () => {
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
@@ -151,22 +143,4 @@ describe('GitHubPreparer', () => {
|
||||
password: 'x-oauth-basic',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the clone command with token from integrations config', async () => {
|
||||
const preparer = new GithubPreparer(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [{ host: 'github.com', token: 'fake-me' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
logger,
|
||||
username: 'fake-me',
|
||||
password: 'x-oauth-basic',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,35 +20,15 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { parseLocationAnnotation } from '../helpers';
|
||||
import { InputError, Git } from '@backstage/backend-common';
|
||||
import { PreparerBase, PreparerOptions } from './types';
|
||||
import { Logger } from 'winston';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
GitHubIntegrationConfig,
|
||||
readGitHubIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
import { GitHubIntegrationConfig } from '@backstage/integration';
|
||||
|
||||
export class GithubPreparer implements PreparerBase {
|
||||
private readonly integrations: GitHubIntegrationConfig[];
|
||||
private readonly scaffolderToken: string | undefined;
|
||||
|
||||
static fromConfig(config: Config, { logger }: { logger: Logger }) {
|
||||
if (config.getOptionalString('scaffolder.github.token')) {
|
||||
logger.warn(
|
||||
"DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead",
|
||||
'Please migrate to using integrations config and specifying tokens under hostnames',
|
||||
);
|
||||
}
|
||||
|
||||
return new GithubPreparer(config);
|
||||
static fromConfig(config: GitHubIntegrationConfig) {
|
||||
return new GithubPreparer(config.token);
|
||||
}
|
||||
|
||||
constructor(config: Config) {
|
||||
this.integrations = readGitHubIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.github') ?? [],
|
||||
);
|
||||
this.scaffolderToken = config.getOptionalString('scaffolder.github.token');
|
||||
}
|
||||
constructor(private readonly token?: string) {}
|
||||
|
||||
async prepare(
|
||||
template: TemplateEntityV1alpha1,
|
||||
@@ -78,11 +58,9 @@ export class GithubPreparer implements PreparerBase {
|
||||
|
||||
const checkoutLocation = path.resolve(tempDir, templateDirectory);
|
||||
|
||||
const token = this.getToken(parsedGitLocation.resource);
|
||||
|
||||
const git = token
|
||||
const git = this.token
|
||||
? Git.fromAuth({
|
||||
username: token,
|
||||
username: this.token,
|
||||
password: 'x-oauth-basic',
|
||||
logger,
|
||||
})
|
||||
@@ -95,10 +73,4 @@ export class GithubPreparer implements PreparerBase {
|
||||
|
||||
return checkoutLocation;
|
||||
}
|
||||
private getToken(host: string): string | undefined {
|
||||
return (
|
||||
this.scaffolderToken ||
|
||||
this.integrations.find(c => c.host === host)?.token
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +78,11 @@ describe('GitLabPreparer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const preparer = GitlabPreparer.fromConfig({
|
||||
host: 'gitlab.com',
|
||||
token: 'fake-token',
|
||||
});
|
||||
it(`calls the clone command with the correct arguments for a repository`, async () => {
|
||||
const preparer = new GitlabPreparer(new ConfigReader({}));
|
||||
mockEntity = mockTemplate();
|
||||
|
||||
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
|
||||
@@ -92,37 +94,6 @@ describe('GitLabPreparer', () => {
|
||||
});
|
||||
|
||||
it(`calls the clone command with the correct arguments if an access token is provided in integrations for a repository`, async () => {
|
||||
const preparer = new GitlabPreparer(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
token: 'fake-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
mockEntity = mockTemplate();
|
||||
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
|
||||
expect(Git.fromAuth).toHaveBeenCalledWith({
|
||||
logger,
|
||||
username: 'oauth2',
|
||||
password: 'fake-token',
|
||||
});
|
||||
});
|
||||
|
||||
it(`calls the clone command with the correct arguments if an access token is provided in scaffolder for a repository`, async () => {
|
||||
const preparer = new GitlabPreparer(
|
||||
new ConfigReader({
|
||||
scaffolder: {
|
||||
gitlab: { api: { token: 'fake-token' } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
mockEntity = mockTemplate();
|
||||
|
||||
await preparer.prepare(mockEntity, { logger });
|
||||
@@ -135,7 +106,6 @@ describe('GitLabPreparer', () => {
|
||||
});
|
||||
|
||||
it(`calls the clone command with the correct arguments for a repository when no path is provided`, async () => {
|
||||
const preparer = new GitlabPreparer(new ConfigReader({}));
|
||||
mockEntity = mockTemplate();
|
||||
delete mockEntity.spec.path;
|
||||
|
||||
@@ -148,7 +118,6 @@ describe('GitLabPreparer', () => {
|
||||
});
|
||||
|
||||
it(`return the temp directory with the path to the folder if it is specified`, async () => {
|
||||
const preparer = new GitlabPreparer(new ConfigReader({}));
|
||||
mockEntity = mockTemplate();
|
||||
mockEntity.spec.path = './template/test/1/2/3';
|
||||
const response = await preparer.prepare(mockEntity, {
|
||||
@@ -160,7 +129,6 @@ describe('GitLabPreparer', () => {
|
||||
});
|
||||
|
||||
it('return the working directory with the path to the folder if it is specified', async () => {
|
||||
const preparer = new GitlabPreparer(new ConfigReader({}));
|
||||
mockEntity.spec.path = './template/test/1/2/3';
|
||||
const response = await preparer.prepare(mockEntity, {
|
||||
workingDirectory: '/workDir',
|
||||
|
||||
@@ -15,43 +15,20 @@
|
||||
*/
|
||||
import { InputError, Git } from '@backstage/backend-common';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
GitLabIntegrationConfig,
|
||||
readGitLabIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
import { GitLabIntegrationConfig } from '@backstage/integration';
|
||||
import fs from 'fs-extra';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { parseLocationAnnotation } from '../helpers';
|
||||
import { PreparerBase, PreparerOptions } from './types';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export class GitlabPreparer implements PreparerBase {
|
||||
private readonly integrations: GitLabIntegrationConfig[];
|
||||
private readonly scaffolderToken: string | undefined;
|
||||
|
||||
static fromConfig(config: Config, { logger }: { logger: Logger }) {
|
||||
if (config.getOptionalString('scaffolder.gitlab.api.token')) {
|
||||
logger.warn(
|
||||
"DEPRECATION: Using the token format under 'scaffolder.gitlab.token' will not be respected in future releases. Please consider using integrations config instead",
|
||||
'Please migrate to using integrations config and specifying tokens under hostnames',
|
||||
);
|
||||
}
|
||||
|
||||
return new GitlabPreparer(config);
|
||||
static fromConfig(config: GitLabIntegrationConfig) {
|
||||
return new GitlabPreparer(config.token);
|
||||
}
|
||||
|
||||
constructor(config: Config) {
|
||||
this.integrations = readGitLabIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.gitlab') ?? [],
|
||||
);
|
||||
|
||||
this.scaffolderToken = config.getOptionalString(
|
||||
'scaffolder.gitlab.api.token',
|
||||
);
|
||||
}
|
||||
constructor(private readonly token?: string) {}
|
||||
|
||||
async prepare(
|
||||
template: TemplateEntityV1alpha1,
|
||||
@@ -79,10 +56,9 @@ export class GitlabPreparer implements PreparerBase {
|
||||
template.spec.path ?? '.',
|
||||
);
|
||||
|
||||
const token = this.getToken(parsedGitLocation.resource);
|
||||
const git = token
|
||||
const git = this.token
|
||||
? Git.fromAuth({
|
||||
password: token,
|
||||
password: this.token,
|
||||
username: 'oauth2',
|
||||
logger,
|
||||
})
|
||||
@@ -95,11 +71,4 @@ export class GitlabPreparer implements PreparerBase {
|
||||
|
||||
return path.resolve(tempDir, templateDirectory);
|
||||
}
|
||||
|
||||
private getToken(host: string): string | undefined {
|
||||
return (
|
||||
this.scaffolderToken ||
|
||||
this.integrations.find(c => c.host === host)?.token
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,107 +16,143 @@
|
||||
import { Preparers } from '.';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { FilePreparer } from './file';
|
||||
import { GithubPreparer } from './github';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('Preparers', () => {
|
||||
const mockTemplate: TemplateEntityV1alpha1 = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Template',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'file:/Users/bingo/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
|
||||
},
|
||||
name: 'react-ssr-template',
|
||||
title: 'React SSR Template',
|
||||
description:
|
||||
'Next.js application skeleton for creating isomorphic web applications.',
|
||||
uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
|
||||
etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
templater: 'cookiecutter',
|
||||
path: '.',
|
||||
type: 'website',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
required: ['storePath', 'owner'],
|
||||
properties: {
|
||||
owner: {
|
||||
type: 'string',
|
||||
title: 'Owner',
|
||||
description: 'Who is going to own this component',
|
||||
},
|
||||
storePath: {
|
||||
type: 'string',
|
||||
title: 'Store path',
|
||||
description: 'GitHub store path in org/repo format',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
it('should throw an error when the preparer for the source location is not registered', () => {
|
||||
const preparers = new Preparers();
|
||||
// const mockTemplate: TemplateEntityV1alpha1 = {
|
||||
// apiVersion: 'backstage.io/v1alpha1',
|
||||
// kind: 'Template',
|
||||
// metadata: {
|
||||
// annotations: {
|
||||
// 'backstage.io/managed-by-location':
|
||||
// 'file:/Users/bingo/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
|
||||
// },
|
||||
// name: 'react-ssr-template',
|
||||
// title: 'React SSR Template',
|
||||
// description:
|
||||
// 'Next.js application skeleton for creating isomorphic web applications.',
|
||||
// uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
|
||||
// etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
|
||||
// generation: 1,
|
||||
// },
|
||||
// spec: {
|
||||
// templater: 'cookiecutter',
|
||||
// path: '.',
|
||||
// type: 'website',
|
||||
// schema: {
|
||||
// $schema: 'http://json-schema.org/draft-07/schema#',
|
||||
// required: ['storePath', 'owner'],
|
||||
// properties: {
|
||||
// owner: {
|
||||
// type: 'string',
|
||||
// title: 'Owner',
|
||||
// description: 'Who is going to own this component',
|
||||
// },
|
||||
// storePath: {
|
||||
// type: 'string',
|
||||
// title: 'Store path',
|
||||
// description: 'GitHub store path in org/repo format',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// it('should throw an error when the preparer for the source location is not registered', () => {
|
||||
// const preparers = new Preparers();
|
||||
|
||||
expect(() => preparers.get(mockTemplate)).toThrow(
|
||||
expect.objectContaining({
|
||||
message: 'No preparer registered for type: "file"',
|
||||
// expect(() => preparers.get(mockTemplate)).toThrow(
|
||||
// expect.objectContaining({
|
||||
// message: 'No preparer registered for type: "file"',
|
||||
// }),
|
||||
// );
|
||||
// });
|
||||
// it('should return the correct preparer when the source matches', () => {
|
||||
// const preparers = new Preparers();
|
||||
// const preparer = new FilePreparer();
|
||||
|
||||
// preparers.register('file', preparer);
|
||||
|
||||
// expect(preparers.get(mockTemplate)).toBe(preparer);
|
||||
// });
|
||||
|
||||
// it('should throw an error if the metadata tag does not exist in the entity', () => {
|
||||
// const brokenTemplate: TemplateEntityV1alpha1 = {
|
||||
// apiVersion: 'backstage.io/v1alpha1',
|
||||
// kind: 'Template',
|
||||
// metadata: {
|
||||
// annotations: {},
|
||||
// name: 'react-ssr-template',
|
||||
// title: 'React SSR Template',
|
||||
// description:
|
||||
// 'Next.js application skeleton for creating isomorphic web applications.',
|
||||
// uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
|
||||
// etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
|
||||
// generation: 1,
|
||||
// },
|
||||
// spec: {
|
||||
// type: 'website',
|
||||
// templater: 'cookiecutter',
|
||||
// path: '.',
|
||||
// schema: {
|
||||
// $schema: 'http://json-schema.org/draft-07/schema#',
|
||||
// required: ['storePath', 'owner'],
|
||||
// properties: {
|
||||
// owner: {
|
||||
// type: 'string',
|
||||
// title: 'Owner',
|
||||
// description: 'Who is going to own this component',
|
||||
// },
|
||||
// storePath: {
|
||||
// type: 'string',
|
||||
// title: 'Store path',
|
||||
// description: 'GitHub store path in org/repo format',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
|
||||
// const preparers = new Preparers();
|
||||
|
||||
// expect(() => preparers.get(brokenTemplate)).toThrow(
|
||||
// expect.objectContaining({
|
||||
// message: expect.stringContaining('No location annotation provided'),
|
||||
// }),
|
||||
// );
|
||||
// });
|
||||
|
||||
it('should return the correct preparer based on the hostname', async () => {
|
||||
const preparer = new GithubPreparer(
|
||||
new ConfigReader({
|
||||
host: 'github.com',
|
||||
apiBaseUrl: 'lols',
|
||||
token: 'something else yo',
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('should return the correct preparer when the source matches', () => {
|
||||
const preparers = new Preparers();
|
||||
const preparer = new FilePreparer();
|
||||
|
||||
preparers.register('file', preparer);
|
||||
|
||||
expect(preparers.get(mockTemplate)).toBe(preparer);
|
||||
});
|
||||
|
||||
it('should throw an error if the metadata tag does not exist in the entity', () => {
|
||||
const brokenTemplate: TemplateEntityV1alpha1 = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Template',
|
||||
metadata: {
|
||||
annotations: {},
|
||||
name: 'react-ssr-template',
|
||||
title: 'React SSR Template',
|
||||
description:
|
||||
'Next.js application skeleton for creating isomorphic web applications.',
|
||||
uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
|
||||
etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
path: '.',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
required: ['storePath', 'owner'],
|
||||
properties: {
|
||||
owner: {
|
||||
type: 'string',
|
||||
title: 'Owner',
|
||||
description: 'Who is going to own this component',
|
||||
},
|
||||
storePath: {
|
||||
type: 'string',
|
||||
title: 'Store path',
|
||||
description: 'GitHub store path in org/repo format',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const preparers = new Preparers();
|
||||
preparers.register('github.com', preparer);
|
||||
|
||||
expect(() => preparers.get(brokenTemplate)).toThrow(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('No location annotation provided'),
|
||||
expect(
|
||||
preparers.get('https://github.com/please/find/me/something/from/github'),
|
||||
).toBe(preparer);
|
||||
});
|
||||
|
||||
it('should throw an error if there is nothing that will match the url provided', async () => {
|
||||
const preparer = new GithubPreparer(
|
||||
new ConfigReader({
|
||||
host: 'github.com',
|
||||
apiBaseUrl: 'lols',
|
||||
token: 'something else yo',
|
||||
}),
|
||||
);
|
||||
|
||||
const preparers = new Preparers();
|
||||
preparers.register('github.com', preparer);
|
||||
|
||||
expect(() => preparers.get('https://404.com')).toThrow(
|
||||
`Unable to find a preparer for URL: https://404.com. Please make sure to register this host under an integration in app-config`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,79 +15,74 @@
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
import { PreparerBase, PreparerBuilder } from './types';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import {
|
||||
DeprecatedLocationTypeDetector,
|
||||
makeDeprecatedLocationTypeDetector,
|
||||
parseLocationAnnotation,
|
||||
} from '../helpers';
|
||||
import { RemoteProtocol } from '../types';
|
||||
import { FilePreparer } from './file';
|
||||
|
||||
import { GitlabPreparer } from './gitlab';
|
||||
import { AzurePreparer } from './azure';
|
||||
import { GithubPreparer } from './github';
|
||||
import { BitbucketPreparer } from './bitbucket';
|
||||
import {
|
||||
readAzureIntegrationConfigs,
|
||||
readBitbucketIntegrationConfigs,
|
||||
readGitHubIntegrationConfigs,
|
||||
readGitLabIntegrationConfigs,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
|
||||
export class Preparers implements PreparerBuilder {
|
||||
private preparerMap = new Map<RemoteProtocol, PreparerBase>();
|
||||
private preparerMap = new Map<string, PreparerBase>();
|
||||
|
||||
constructor(private readonly typeDetector?: DeprecatedLocationTypeDetector) {}
|
||||
|
||||
register(protocol: RemoteProtocol, preparer: PreparerBase) {
|
||||
this.preparerMap.set(protocol, preparer);
|
||||
register(host: string, preparer: PreparerBase) {
|
||||
this.preparerMap.set(host, preparer);
|
||||
}
|
||||
|
||||
get(template: TemplateEntityV1alpha1): PreparerBase {
|
||||
const { protocol, location } = parseLocationAnnotation(template);
|
||||
|
||||
const preparer = this.preparerMap.get(protocol);
|
||||
|
||||
get(url: string): PreparerBase {
|
||||
const preparer = this.preparerMap.get(new URL(url).host);
|
||||
if (!preparer) {
|
||||
if ((protocol as string) === 'url') {
|
||||
const type = this.typeDetector?.(location);
|
||||
const detected = type && this.preparerMap.get(type as RemoteProtocol);
|
||||
if (detected) {
|
||||
return detected;
|
||||
}
|
||||
if (type) {
|
||||
throw new Error(
|
||||
`No preparer configuration available for type '${type}' with url "${location}". ` +
|
||||
"Make sure you've added appropriate configuration in the 'scaffolder' configuration section",
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to detect preparer type. Unable to determine integration type for location "${location}". ` +
|
||||
"Please add appropriate configuration to the 'integrations' configuration section",
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error(`No preparer registered for type: "${protocol}"`);
|
||||
throw new Error(
|
||||
`Unable to find a preparer for URL: ${url}. Please make sure to register this host under an integration in app-config`,
|
||||
);
|
||||
}
|
||||
|
||||
return preparer;
|
||||
}
|
||||
|
||||
static async fromConfig(
|
||||
config: Config,
|
||||
{ logger }: { logger: Logger },
|
||||
// { logger }: { logger: Logger },
|
||||
): Promise<PreparerBuilder> {
|
||||
const typeDetector = makeDeprecatedLocationTypeDetector(config);
|
||||
// TODO(blam/jhaals)
|
||||
// iterate through the integration configs for each provider, and create the preparer
|
||||
// register the preparer to hostname.
|
||||
// if no preparer is returned for the hostname fromConfig, use the backup token credentials from scaffolder block and warn
|
||||
const preparers = new Preparers();
|
||||
const scm = ScmIntegrations.fromConfig(config);
|
||||
for (const integration of scm.azure.list()) {
|
||||
preparers.register(
|
||||
integration.config.host,
|
||||
AzurePreparer.fromConfig(integration.config),
|
||||
);
|
||||
}
|
||||
|
||||
const preparers = new Preparers(typeDetector);
|
||||
for (const integration of scm.github.list()) {
|
||||
preparers.register(
|
||||
integration.config.host,
|
||||
GithubPreparer.fromConfig(integration.config),
|
||||
);
|
||||
}
|
||||
|
||||
const filePreparer = new FilePreparer();
|
||||
const gitlabPreparer = GitlabPreparer.fromConfig(config, { logger });
|
||||
const azurePreparer = AzurePreparer.fromConfig(config, { logger });
|
||||
const githubPreparer = GithubPreparer.fromConfig(config, { logger });
|
||||
const bitbucketPreparer = BitbucketPreparer.fromConfig(config, { logger });
|
||||
for (const integration of scm.gitlab.list()) {
|
||||
preparers.register(
|
||||
integration.config.host,
|
||||
GitlabPreparer.fromConfig(integration),
|
||||
);
|
||||
}
|
||||
|
||||
preparers.register('file', filePreparer);
|
||||
preparers.register('gitlab', gitlabPreparer);
|
||||
preparers.register('azure', azurePreparer);
|
||||
preparers.register('github', githubPreparer);
|
||||
preparers.register('bitbucket', bitbucketPreparer);
|
||||
for (const integration of scm.bitbucket.list()) {
|
||||
preparers.register(
|
||||
integration.config.host,
|
||||
BitbucketPreparer.fromConfig(integration.config),
|
||||
);
|
||||
}
|
||||
|
||||
return preparers;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,6 @@ export interface PreparerBase {
|
||||
}
|
||||
|
||||
export type PreparerBuilder = {
|
||||
register(protocol: RemoteProtocol, preparer: PreparerBase): void;
|
||||
get(template: TemplateEntityV1alpha1): PreparerBase;
|
||||
register(host: string, preparer: PreparerBase): void;
|
||||
get(url: string): PreparerBase;
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@ import { AzurePublisher } from './azure';
|
||||
import { WebApi } from 'azure-devops-node-api';
|
||||
import * as helpers from './helpers';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('Azure Publisher', () => {
|
||||
const logger = getVoidLogger();
|
||||
@@ -40,23 +39,16 @@ describe('Azure Publisher', () => {
|
||||
|
||||
((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi);
|
||||
|
||||
const publisher = new AzurePublisher(
|
||||
new ConfigReader({
|
||||
scaffolder: {
|
||||
azure: {
|
||||
api: {
|
||||
baseUrl: 'https://dev.azure.com/myorg',
|
||||
token: 'fake-azure-token',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const publisher = await AzurePublisher.fromConfig({
|
||||
host: 'dev.azure.com',
|
||||
token: 'fake-azure-token',
|
||||
});
|
||||
|
||||
mockGitClient.createRepository.mockResolvedValue({
|
||||
remoteUrl: 'https://dev.azure.com/organization/project/_git/repo',
|
||||
} as { remoteUrl: string });
|
||||
|
||||
const result = await publisher.publish({
|
||||
const result = await publisher!.publish({
|
||||
values: {
|
||||
storePath: 'project/repo',
|
||||
owner: 'bob',
|
||||
@@ -83,59 +75,5 @@ describe('Azure Publisher', () => {
|
||||
logger,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use azure-devops-node-api with integrations config', async () => {
|
||||
const mockGitClient = {
|
||||
createRepository: jest.fn(),
|
||||
};
|
||||
const mockGitApi = {
|
||||
getGitApi: jest.fn().mockReturnValue(mockGitClient),
|
||||
};
|
||||
|
||||
((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi);
|
||||
|
||||
const publisher = new AzurePublisher(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
azure: [
|
||||
{
|
||||
host: 'dev.azure.com',
|
||||
token: 'fake-azure-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
mockGitClient.createRepository.mockResolvedValue({
|
||||
remoteUrl: 'https://dev.azure.com/organization/project/_git/repo',
|
||||
} as { remoteUrl: string });
|
||||
|
||||
const result = await publisher.publish({
|
||||
values: {
|
||||
storePath: 'https://dev.azure.com/organization/project/_git/repo',
|
||||
owner: 'bob',
|
||||
},
|
||||
directory: '/tmp/test',
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
remoteUrl: 'https://dev.azure.com/organization/project/_git/repo',
|
||||
catalogInfoUrl:
|
||||
'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml',
|
||||
});
|
||||
expect(mockGitClient.createRepository).toHaveBeenCalledWith(
|
||||
{
|
||||
name: 'repo',
|
||||
},
|
||||
'project',
|
||||
);
|
||||
expect(helpers.initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: '/tmp/test',
|
||||
remoteUrl: 'https://dev.azure.com/organization/project/_git/repo',
|
||||
auth: { username: 'notempty', password: 'fake-azure-token' },
|
||||
logger,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,53 +17,35 @@
|
||||
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
|
||||
import { IGitApi } from 'azure-devops-node-api/GitApi';
|
||||
import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces';
|
||||
import { Config } from '@backstage/config';
|
||||
import { initRepoAndPush } from './helpers';
|
||||
import {
|
||||
AzureIntegrationConfig,
|
||||
readAzureIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
import { AzureIntegrationConfig } from '@backstage/integration';
|
||||
import gitUrlParse from 'git-url-parse';
|
||||
import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api';
|
||||
|
||||
export class AzurePublisher implements PublisherBase {
|
||||
private readonly integrations: AzureIntegrationConfig[];
|
||||
private readonly apiBaseUrl?: string;
|
||||
private readonly token?: string;
|
||||
|
||||
static fromConfig(config: Config) {
|
||||
return new AzurePublisher(config);
|
||||
static async fromConfig(config: AzureIntegrationConfig) {
|
||||
if (!config.token) {
|
||||
return undefined;
|
||||
}
|
||||
const authHandler = getPersonalAccessTokenHandler(config.token);
|
||||
const webApi = new WebApi(config.host, authHandler);
|
||||
const azureClient = await webApi.getGitApi();
|
||||
return new AzurePublisher(config.token, azureClient);
|
||||
}
|
||||
|
||||
constructor(config: Config) {
|
||||
this.integrations = readAzureIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.azure') ?? [],
|
||||
);
|
||||
constructor(
|
||||
private readonly token: string,
|
||||
private readonly azureClient: IGitApi,
|
||||
) {}
|
||||
|
||||
this.token = config.getOptionalString('scaffolder.azure.api.token');
|
||||
this.apiBaseUrl = config.getOptionalString('scaffolder.azure.api.baseUrl');
|
||||
}
|
||||
async publish({
|
||||
values,
|
||||
directory,
|
||||
logger,
|
||||
}: PublisherOptions): Promise<PublisherResult> {
|
||||
const { resource: host, owner, name } = gitUrlParse(values.storePath);
|
||||
const { owner, name } = gitUrlParse(values.storePath);
|
||||
|
||||
const token = this.getToken(host);
|
||||
if (!token) {
|
||||
throw new Error('No token provided to create the remote repository');
|
||||
}
|
||||
const baseUrl = this.getBaseUrl(host);
|
||||
if (!baseUrl) {
|
||||
throw new Error('No baseUrl provided to create the remote repository');
|
||||
}
|
||||
|
||||
const authHandler = getPersonalAccessTokenHandler(token);
|
||||
const webApi = new WebApi(baseUrl, authHandler);
|
||||
const azureClient = await webApi.getGitApi();
|
||||
|
||||
const remoteUrl = await this.createRemote(azureClient, {
|
||||
const remoteUrl = await this.createRemote({
|
||||
project: owner,
|
||||
name,
|
||||
});
|
||||
@@ -75,7 +57,7 @@ export class AzurePublisher implements PublisherBase {
|
||||
remoteUrl,
|
||||
auth: {
|
||||
username: 'notempty',
|
||||
password: token,
|
||||
password: this.token,
|
||||
},
|
||||
logger,
|
||||
});
|
||||
@@ -83,24 +65,14 @@ export class AzurePublisher implements PublisherBase {
|
||||
return { remoteUrl, catalogInfoUrl };
|
||||
}
|
||||
|
||||
private async createRemote(
|
||||
client: IGitApi,
|
||||
opts: { name: string; project: string },
|
||||
) {
|
||||
private async createRemote(opts: { name: string; project: string }) {
|
||||
const { name, project } = opts;
|
||||
const createOptions: GitRepositoryCreateOptions = { name };
|
||||
const repo = await client.createRepository(createOptions, project);
|
||||
const repo = await this.azureClient.createRepository(
|
||||
createOptions,
|
||||
project,
|
||||
);
|
||||
|
||||
return repo.remoteUrl || '';
|
||||
}
|
||||
|
||||
private getToken(host: string): string | undefined {
|
||||
return this.token || this.integrations.find(c => c.host === host)?.token;
|
||||
}
|
||||
|
||||
private getBaseUrl(host: string): string | undefined {
|
||||
return (
|
||||
this.apiBaseUrl || this.integrations.find(c => c.host === host)?.host
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
StageContext,
|
||||
TemplaterBuilder,
|
||||
PublisherBuilder,
|
||||
parseLocationAnnotation,
|
||||
} from '../scaffolder';
|
||||
import { CatalogEntityClient } from '../lib/catalog';
|
||||
import { validate, ValidatorResult } from 'jsonschema';
|
||||
@@ -129,7 +130,13 @@ export async function createRouter(
|
||||
{
|
||||
name: 'Prepare the skeleton',
|
||||
handler: async ctx => {
|
||||
const preparer = preparers.get(ctx.entity);
|
||||
const { protocol, location: pullPath } = parseLocationAnnotation(
|
||||
ctx.entity,
|
||||
);
|
||||
|
||||
const preparer =
|
||||
protcol === file ? new FilePreparer() : preparers.get(pullPath);
|
||||
|
||||
const skeletonDir = await preparer.prepare(ctx.entity, {
|
||||
logger: ctx.logger,
|
||||
workingDirectory,
|
||||
|
||||
Reference in New Issue
Block a user