Add GitLab integration for scaffolder

This adds a GitLab integration for the scaffolder backend.

We're introduceing a preparer and a publisher for GitLab so that we can
read templates from GitLab and publish them to a configured GitLab
instance. The two instances don't need to be the same. For instance,
templates could be public on gitlab.com, but the created repos will live
in a hosted GitLab somewhere else.

The publisher gets its own config object in `app-config.yaml` where the
target instance and token can be specified.

The service catalogue defines both `gitlab` and `gitlab/api` as
processors. They are both handled by the same preparer.

Closes #2372
This commit is contained in:
Björn Marschollek
2020-09-07 08:18:03 +02:00
parent f1ca8daee8
commit 55542797a9
30 changed files with 945 additions and 29 deletions
+3 -1
View File
@@ -23,6 +23,8 @@
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@gitbeaker/core": "^23.5.0",
"@gitbeaker/node": "^23.5.0",
"@octokit/rest": "^18.0.0",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
@@ -33,7 +35,7 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
"git-url-parse": "^11.1.2",
"git-url-parse": "^11.2.0",
"globby": "^11.0.0",
"helmet": "^4.0.0",
"jsonschema": "^1.2.6",
@@ -0,0 +1,33 @@
/*
* 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.
*/
export const mockGitlabClient = {
Namespaces: {
show: jest.fn(),
},
Projects: {
create: jest.fn(),
},
Users: {
current: jest.fn(),
},
};
export class Gitlab {
constructor() {
return mockGitlabClient;
}
}
@@ -16,3 +16,4 @@
export * from './prepare';
export * from './publish';
export * from './templater';
export * from './helpers';
@@ -17,7 +17,7 @@ import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from './helpers';
import { parseLocationAnnotation } from '../helpers';
import { InputError } from '@backstage/backend-common';
import { PreparerBase } from './types';
@@ -17,7 +17,7 @@ import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from './helpers';
import { parseLocationAnnotation } from '../helpers';
import { InputError } from '@backstage/backend-common';
import { PreparerBase } from './types';
import GitUriParser from 'git-url-parse';
@@ -0,0 +1,143 @@
/*
* 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.
*/
const mocks = {
Clone: { clone: jest.fn() },
CheckoutOptions: jest.fn(() => {}),
};
jest.doMock('nodegit', () => mocks);
import { GitlabPreparer } from './gitlab';
import {
TemplateEntityV1alpha1,
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
const mockEntityWithProtocol = (protocol: string): TemplateEntityV1alpha1 => ({
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {
[LOCATION_ANNOTATION]: `${protocol}:https://gitlab.com/benjdlambert/backstage-graphql-template/-/blob/master/template.yaml`,
},
name: 'graphql-starter',
title: 'GraphQL Service',
description:
'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n',
uid: '9cf16bad-16e0-4213-b314-c4eec773c50b',
etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2',
generation: 1,
},
spec: {
type: 'website',
templater: 'cookiecutter',
path: './template',
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',
},
},
},
},
});
describe('GitLabPreparer', () => {
let mockEntity: TemplateEntityV1alpha1;
beforeEach(() => {
jest.clearAllMocks();
});
['gitlab', 'gitlab/api'].forEach(protocol => {
it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => {
const preparer = new GitlabPreparer(ConfigReader.fromConfigs([]));
mockEntity = mockEntityWithProtocol(protocol);
await preparer.prepare(mockEntity);
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://gitlab.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{},
);
});
it(`calls the clone command with the correct arguments if an access token is provided for a repository using the ${protocol} protocol`, async () => {
const preparer = new GitlabPreparer(
ConfigReader.fromConfigs([
{
context: '',
data: {
catalog: {
processors: {
gitlabApi: {
privateToken: 'fake-token',
},
},
},
},
},
]),
);
mockEntity = mockEntityWithProtocol(protocol);
await preparer.prepare(mockEntity);
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://gitlab.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{
fetchOpts: {
callbacks: {
credentials: expect.anything(),
},
},
},
);
});
it(`calls the clone command with the correct arguments for a repository when no path is provided using the ${protocol} protocol`, async () => {
const preparer = new GitlabPreparer(ConfigReader.fromConfigs([]));
mockEntity = mockEntityWithProtocol(protocol);
delete mockEntity.spec.path;
await preparer.prepare(mockEntity);
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://gitlab.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{},
);
});
it(`return the temp directory with the path to the folder if it is specified using the ${protocol} protocol`, async () => {
const preparer = new GitlabPreparer(ConfigReader.fromConfigs([]));
mockEntity = mockEntityWithProtocol(protocol);
mockEntity.spec.path = './template/test/1/2/3';
const response = await preparer.prepare(mockEntity);
expect(response.split('\\').join('/')).toMatch(
/\/template\/test\/1\/2\/3$/,
);
});
});
});
@@ -0,0 +1,73 @@
/*
* 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 fs from 'fs-extra';
import path from 'path';
import os from 'os';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from '../helpers';
import { InputError } from '@backstage/backend-common';
import { PreparerBase } from './types';
import GitUriParser from 'git-url-parse';
import { Clone, Cred } from 'nodegit';
import { Config } from '@backstage/config';
export class GitlabPreparer implements PreparerBase {
private readonly privateToken: string;
constructor(config: Config) {
this.privateToken =
config.getOptionalString('catalog.processors.gitlabApi.privateToken') ??
'';
}
async prepare(template: TemplateEntityV1alpha1): Promise<string> {
const { protocol, location } = parseLocationAnnotation(template);
if (['gitlab', 'gitlab/api'].indexOf(protocol) < 0) {
throw new InputError(
`Wrong location protocol: ${protocol}, should be 'gitlab' or 'gitlab/api'`,
);
}
const templateId = template.metadata.name;
const parsedGitLocation = GitUriParser(location);
const repositoryCheckoutUrl = parsedGitLocation.toString('https');
const tempDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), templateId),
);
const templateDirectory = path.join(
`${path.dirname(parsedGitLocation.filepath)}`,
template.spec.path ?? '.',
);
const options = this.privateToken
? {
fetchOpts: {
callbacks: {
credentials: () =>
Cred.userpassPlaintextNew('oauth2', this.privateToken),
},
},
}
: {};
await Clone.clone(repositoryCheckoutUrl, tempDir, options);
return path.resolve(tempDir, templateDirectory);
}
}
@@ -15,6 +15,6 @@
*/
export * from './preparers';
export * from './types';
export * from './helpers';
export * from './file';
export * from './github';
export * from './gitlab';
@@ -14,9 +14,10 @@
* limitations under the License.
*/
import { PreparerBase, RemoteProtocol, PreparerBuilder } from './types';
import { PreparerBase, PreparerBuilder } from './types';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from './helpers';
import { parseLocationAnnotation } from '../helpers';
import { RemoteProtocol } from '../types';
export class Preparers implements PreparerBuilder {
private preparerMap = new Map<RemoteProtocol, PreparerBase>();
@@ -15,6 +15,7 @@
*/
import type { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { Logger } from 'winston';
import { RemoteProtocol } from '../types';
export type PreparerBase = {
/**
@@ -32,5 +33,3 @@ export type PreparerBuilder = {
register(protocol: RemoteProtocol, preparer: PreparerBase): void;
get(template: TemplateEntityV1alpha1): PreparerBase;
};
export type RemoteProtocol = 'file' | 'github';
@@ -0,0 +1,205 @@
/*
* 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.
*/
jest.mock('nodegit');
jest.mock('@gitbeaker/node');
import { GitlabPublisher } from './gitlab';
import { Gitlab as GitlabAPI } from '@gitbeaker/core';
import { Gitlab } from '@gitbeaker/node';
import * as NodeGit from 'nodegit';
const { mockGitlabClient } = require('@gitbeaker/node') as {
mockGitlabClient: {
Namespaces: jest.Mocked<GitlabAPI['Namespaces']>;
Projects: jest.Mocked<GitlabAPI['Projects']>;
Users: jest.Mocked<GitlabAPI['Users']>;
};
};
const {
Repository,
mockRepo,
mockIndex,
Signature,
Remote,
mockRemote,
Cred,
} = require('nodegit') as {
Repository: jest.Mocked<{ init: any }>;
Signature: jest.Mocked<{ now: any }>;
Cred: jest.Mocked<{ userpassPlaintextNew: any }>;
Remote: jest.Mocked<{ create: any }>;
mockIndex: jest.Mocked<NodeGit.Index>;
mockRepo: jest.Mocked<NodeGit.Repository>;
mockRemote: jest.Mocked<NodeGit.Remote>;
};
describe('GitLab Publisher', () => {
const publisher = new GitlabPublisher(new Gitlab({}), 'fake-token');
beforeEach(() => {
jest.clearAllMocks();
});
describe('publish: createRemoteInGitLab', () => {
it('should use gitbeaker to create a repo in a namespace if the namespace property is set', async () => {
mockGitlabClient.Namespaces.show.mockResolvedValue({
id: 42,
} as { id: number });
await publisher.publish({
values: {
isOrg: true,
storePath: 'blam/test',
owner: 'bob',
},
directory: '/tmp/test',
});
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
namespace_id: 42,
name: 'test',
});
});
it('should use gitbeaker to create a repo in the authed user if the namespace property is not set', async () => {
mockGitlabClient.Namespaces.show.mockResolvedValue({});
mockGitlabClient.Users.current.mockResolvedValue({
id: 21,
} as { id: number });
mockGitlabClient.Projects.create.mockResolvedValue({
http_url_to_repo: 'mockclone',
} as { http_url_to_repo: string });
await publisher.publish({
values: {
storePath: 'blam/test',
owner: 'bob',
},
directory: '/tmp/test',
});
expect(mockGitlabClient.Users.current).toHaveBeenCalled();
expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({
namespace_id: 21,
name: 'test',
});
});
});
describe('publish: createGitDirectory', () => {
const values = {
isOrg: true,
storePath: 'blam/test',
owner: 'lols',
};
const mockDir = '/tmp/test/dir';
mockGitlabClient.Projects.create.mockResolvedValue({
http_url_to_repo: 'mockclone',
} as { http_url_to_repo: string });
it('should call init on the repo with the directory', async () => {
await publisher.publish({
values,
directory: mockDir,
});
expect(Repository.init).toHaveBeenCalledWith(mockDir, 0);
});
it('should call refresh index on the index and write the new files', async () => {
await publisher.publish({
values,
directory: mockDir,
});
expect(mockRepo.refreshIndex).toHaveBeenCalled();
});
it('should call add all files and write', async () => {
await publisher.publish({
values,
directory: mockDir,
});
expect(mockIndex.addAll).toHaveBeenCalled();
expect(mockIndex.write).toHaveBeenCalled();
expect(mockIndex.writeTree).toHaveBeenCalled();
});
it('should create a commit with on head with the right name and commiter', async () => {
const mockSignature = { mockSignature: 'bloblly' };
Signature.now.mockReturnValue(mockSignature);
await publisher.publish({
values,
directory: mockDir,
});
expect(Signature.now).toHaveBeenCalledTimes(2);
expect(Signature.now).toHaveBeenCalledWith(
'Scaffolder',
'scaffolder@backstage.io',
);
expect(mockRepo.createCommit).toHaveBeenCalledWith(
'HEAD',
mockSignature,
mockSignature,
'initial commit',
'mockoid',
[],
);
});
it('creates a remote with the repo and remote', async () => {
await publisher.publish({
values,
directory: mockDir,
});
expect(Remote.create).toHaveBeenCalledWith(
mockRepo,
'origin',
'mockclone',
);
});
it('shoud push to the remote repo', async () => {
await publisher.publish({
values,
directory: mockDir,
});
const [remotes, { callbacks }] = mockRemote.push.mock
.calls[0] as NodeGit.PushOptions[];
expect(remotes).toEqual(['refs/heads/master:refs/heads/master']);
callbacks?.credentials?.();
expect(Cred.userpassPlaintextNew).toHaveBeenCalledWith(
'oauth2',
'fake-token',
);
});
});
});
@@ -0,0 +1,90 @@
/*
* 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 } from './types';
import { Gitlab } from '@gitbeaker/core';
import { JsonValue } from '@backstage/config';
import { RequiredTemplateValues } from '../templater';
import { Repository, Remote, Signature, Cred } from 'nodegit';
export class GitlabPublisher implements PublisherBase {
private readonly client: Gitlab;
private readonly token: string;
constructor(client: Gitlab, token: string) {
this.client = client;
this.token = token;
}
async publish({
values,
directory,
}: {
values: RequiredTemplateValues & Record<string, JsonValue>;
directory: string;
}): Promise<{ remoteUrl: string }> {
const remoteUrl = await this.createRemote(values);
await this.pushToRemote(directory, remoteUrl);
return { remoteUrl };
}
private async createRemote(
values: RequiredTemplateValues & Record<string, JsonValue>,
) {
const [owner, name] = values.storePath.split('/');
let targetNamespace = ((await this.client.Namespaces.show(owner)) as {
id: number;
}).id;
if (!targetNamespace) {
targetNamespace = ((await this.client.Users.current()) as { id: number })
.id;
}
const project = (await this.client.Projects.create({
namespace_id: targetNamespace,
name: name,
})) as { http_url_to_repo: string };
return project?.http_url_to_repo;
}
private async pushToRemote(directory: string, remote: string): Promise<void> {
const repo = await Repository.init(directory, 0);
const index = await repo.refreshIndex();
await index.addAll();
await index.write();
const oid = await index.writeTree();
await repo.createCommit(
'HEAD',
Signature.now('Scaffolder', 'scaffolder@backstage.io'),
Signature.now('Scaffolder', 'scaffolder@backstage.io'),
'initial commit',
oid,
[],
);
const remoteRepo = await Remote.create(repo, 'origin', remote);
await remoteRepo.push(['refs/heads/master:refs/heads/master'], {
callbacks: {
credentials: () => Cred.userpassPlaintextNew('oauth2', this.token),
},
});
}
}
@@ -13,5 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './publishers';
export * from './github';
export * from './gitlab';
export * from './types';
@@ -0,0 +1,133 @@
/*
* 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 { Publishers } from './publishers';
import {
LOCATION_ANNOTATION,
TemplateEntityV1alpha1,
} from '@backstage/catalog-model';
import { GithubPublisher } from './github';
import { Octokit } from '@octokit/rest';
jest.mock('@octokit/rest');
describe('Publishers', () => {
const mockTemplate: TemplateEntityV1alpha1 = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {
[LOCATION_ANNOTATION]:
'github:https://github.com/benjdlambert/backstage-graphql-template/blob/master/template.yaml',
},
name: 'graphql-starter',
title: 'GraphQL Service',
description:
'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n',
uid: '9cf16bad-16e0-4213-b314-c4eec773c50b',
etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2',
generation: 1,
},
spec: {
type: 'website',
templater: 'cookiecutter',
path: './template',
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 publisher for the source location is not registered', () => {
const publishers = new Publishers();
expect(() => publishers.get(mockTemplate)).toThrow(
expect.objectContaining({
message: 'No publisher registered for type: "github"',
}),
);
});
it('should return the correct preparer when the source matches', () => {
const publishers = new Publishers();
const publisher = new GithubPublisher({
client: new Octokit(),
token: 'fake',
repoVisibility: 'public',
});
publishers.register('github', publisher);
expect(publishers.get(mockTemplate)).toBe(publisher);
});
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 publishers = new Publishers();
expect(() => publishers.get(brokenTemplate)).toThrow(
expect.objectContaining({
message: expect.stringContaining('No location annotation provided'),
}),
);
});
});
@@ -0,0 +1,39 @@
/*
* 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 { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from '../helpers';
import { PublisherBase, PublisherBuilder } from './types';
import { RemoteProtocol } from '../types';
export class Publishers implements PublisherBuilder {
private publisherMap = new Map<RemoteProtocol, PublisherBase>();
register(protocol: RemoteProtocol, publisher: PublisherBase) {
this.publisherMap.set(protocol, publisher);
}
get(template: TemplateEntityV1alpha1): PublisherBase {
const { protocol } = parseLocationAnnotation(template);
const publisher = this.publisherMap.get(protocol);
if (!publisher) {
throw new Error(`No publisher registered for type: "${protocol}"`);
}
return publisher;
}
}
@@ -16,6 +16,7 @@
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { RequiredTemplateValues } from '../templater';
import { JsonValue } from '@backstage/config';
import { RemoteProtocol } from '../types';
/**
* Publisher is in charge of taking a folder created by
@@ -34,3 +35,8 @@ export type PublisherBase = {
directory: string;
}): Promise<{ remoteUrl: string }>;
};
export type PublisherBuilder = {
register(protocol: RemoteProtocol, publisher: PublisherBase): void;
get(template: TemplateEntityV1alpha1): PublisherBase;
};
@@ -0,0 +1,16 @@
/*
* 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.
*/
export type RemoteProtocol = 'file' | 'github' | 'gitlab' | 'gitlab/api';
@@ -18,21 +18,20 @@ import { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
import { Templaters, Preparers, PublisherBase } from '../scaffolder';
import { Templaters, Preparers, Publishers } from '../scaffolder';
import Docker from 'dockerode';
jest.mock('dockerode');
describe('createRouter', () => {
let app: express.Express;
const publisher: jest.Mocked<PublisherBase> = { publish: jest.fn() };
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
preparers: new Preparers(),
templaters: new Templaters(),
publisher: publisher,
publishers: new Publishers(),
dockerClient: new Docker(),
});
app = express().use(router);
@@ -26,14 +26,14 @@ import {
RequiredTemplateValues,
StageContext,
TemplaterBuilder,
PublisherBase,
PublisherBuilder,
} from '../scaffolder';
import { validate, ValidatorResult } from 'jsonschema';
export interface RouterOptions {
preparers: PreparerBuilder;
templaters: TemplaterBuilder;
publisher: PublisherBase;
publishers: PublisherBuilder;
logger: Logger;
dockerClient: Docker;
@@ -48,7 +48,7 @@ export async function createRouter(
const {
preparers,
templaters,
publisher,
publishers,
logger: parentLogger,
dockerClient,
} = options;
@@ -125,6 +125,7 @@ export async function createRouter(
{
name: 'Publish template',
handler: async (ctx: StageContext<{ resultDir: string }>) => {
const publisher = publishers.get(ctx.entity);
ctx.logger.info('Will now store the template');
const { remoteUrl } = await publisher.publish({
entity: ctx.entity,
+1 -1
View File
@@ -30,7 +30,7 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.1",
"git-url-parse": "^11.1.3",
"git-url-parse": "^11.2.0",
"knex": "^0.21.1",
"node-fetch": "^2.6.0",
"nodegit": "^0.27.0",