feat(scaffolder-backend-module-gitlab): Add gitlab scaffolder action to migrate groups and projects (bulk_imports)
Signed-off-by: mpsanchis <miguelpsanchis@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
|
||||
---
|
||||
|
||||
Added action `gitlab:group:migrate` to migrate projects using `bulk_import`
|
||||
@@ -88,6 +88,20 @@ export const createGitlabProjectDeployTokenAction: (options: {
|
||||
}
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const createGitlabProjectMigrateAction: (options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}) => TemplateAction<
|
||||
{
|
||||
destinationAccessToken: string;
|
||||
destinationUrl: string;
|
||||
sourceAccessToken: string;
|
||||
sourceFullPath: string;
|
||||
sourceUrl: string;
|
||||
},
|
||||
JsonObject
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const createGitlabProjectVariableAction: (options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 { ConfigReader } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { createGitlabProjectMigrateAction } from './gitlabProjectMigrate';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { examples } from './gitlabProjectMigrate.examples';
|
||||
import yaml from 'yaml';
|
||||
|
||||
const mockGitlabClient = {
|
||||
Migrations: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@gitbeaker/rest', () => ({
|
||||
Gitlab: class {
|
||||
constructor() {
|
||||
return mockGitlabClient;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('gitlab:group:migrate', () => {
|
||||
let instance: TemplateAction<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.local.com',
|
||||
token: 'token',
|
||||
apiBaseUrl: 'https://api.gitlab.local.com',
|
||||
},
|
||||
{
|
||||
host: 'hosted.gitlab.com',
|
||||
apiBaseUrl: 'https://api.hosted.gitlab.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
instance = createGitlabProjectMigrateAction({ integrations });
|
||||
});
|
||||
|
||||
describe('Migrate a gitlab project to a different gitlab instance in another', () => {
|
||||
const workspacePath = createMockDirectory().resolve('workspace');
|
||||
|
||||
it(`Execute example ${examples[0].description}`, async () => {
|
||||
const input = yaml.parse(examples[0].example).steps[0].input;
|
||||
const ctx = createMockActionContext({ input, workspacePath });
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(mockGitlabClient.Migrations.create).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://gitlab.remote.com',
|
||||
access_token: 'lolstoken',
|
||||
},
|
||||
[
|
||||
{
|
||||
sourceType: 'project_entity',
|
||||
sourceFullPath: 'my/wonderful-repo',
|
||||
destinationSlug: 'precious-repo',
|
||||
destinationNamespace: 'my/local/site',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 { TemplateExample } from '@backstage/plugin-scaffolder-node';
|
||||
import yaml from 'yaml';
|
||||
|
||||
export const examples: TemplateExample[] = [
|
||||
{
|
||||
description: 'Import repo from remote GitLab instance',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'importGitLabRepo',
|
||||
action: 'gitlab:repo:push',
|
||||
name: 'Push changes to gitlab repository',
|
||||
input: {
|
||||
destinationAccessToken: 'tokenslols',
|
||||
destinationUrl:
|
||||
'gitlab.local.com?repo=precious-repo&owner=my%2Flocal%2Fsite',
|
||||
sourceAccessToken: 'lolstoken',
|
||||
sourceFullPath: 'my/wonderful-repo',
|
||||
sourceUrl: 'https://gitlab.remote.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 { ConfigReader } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { createGitlabProjectMigrateAction } from './gitlabProjectMigrate';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
const mockGitlabClient = {
|
||||
Migrations: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@gitbeaker/rest', () => ({
|
||||
Gitlab: class {
|
||||
constructor() {
|
||||
return mockGitlabClient;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('createGitlabRepoMigrate', () => {
|
||||
let instance: TemplateAction<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
token: 'token',
|
||||
apiBaseUrl: 'https://api.gitlab.com',
|
||||
},
|
||||
{
|
||||
host: 'hosted.gitlab.com',
|
||||
apiBaseUrl: 'https://api.hosted.gitlab.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
instance = createGitlabProjectMigrateAction({ integrations });
|
||||
});
|
||||
|
||||
describe('createGitlabRepoImportAction', () => {
|
||||
const workspacePath = createMockDirectory().resolve('workspace');
|
||||
|
||||
it('default repo import action is created', async () => {
|
||||
const input = {
|
||||
destinationAccessToken: 'moreLOLsToken',
|
||||
destinationUrl:
|
||||
'gitlab.com?owner=migrated%2Ffoo%2Fbar&repo=migrated-go-lang',
|
||||
sourceUrl: 'https://gitlab.remote.com',
|
||||
sourceAccessToken: 'lolstoken',
|
||||
sourceFullPath: 'foo/bar/go-lang',
|
||||
};
|
||||
const ctx = createMockActionContext({ input, workspacePath });
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(mockGitlabClient.Migrations.create).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://gitlab.remote.com',
|
||||
access_token: 'lolstoken',
|
||||
},
|
||||
[
|
||||
{
|
||||
sourceType: 'project_entity',
|
||||
sourceFullPath: 'foo/bar/go-lang',
|
||||
destinationSlug: 'migrated-go-lang',
|
||||
destinationNamespace: 'migrated/foo/bar',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
createTemplateAction,
|
||||
parseRepoUrl,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { createGitlabApi } from './helpers';
|
||||
import { examples } from './gitlabRepoPush.examples';
|
||||
import { MigrationEntityOptions } from '@gitbeaker/rest';
|
||||
|
||||
/**
|
||||
* Create a new action that imports a gitlab project into another gitlab project (potentially from another gitlab instance).
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const createGitlabProjectMigrateAction = (options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}) => {
|
||||
const { integrations } = options;
|
||||
|
||||
return createTemplateAction<{
|
||||
destinationAccessToken: string;
|
||||
destinationUrl: string;
|
||||
sourceAccessToken: string;
|
||||
sourceFullPath: string;
|
||||
sourceUrl: string;
|
||||
}>({
|
||||
id: 'gitlab:group:migrate',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
required: [
|
||||
'destinationAccessToken',
|
||||
'destinationUrl',
|
||||
'sourceAccessToken',
|
||||
'sourceFullPath',
|
||||
'sourceUrl',
|
||||
],
|
||||
type: 'object',
|
||||
properties: {
|
||||
destinationAccessToken: {
|
||||
type: 'string',
|
||||
title: 'Target Repository Access Token',
|
||||
description: `The token to use for authorization to the target GitLab'`,
|
||||
},
|
||||
destinationUrl: {
|
||||
type: 'string',
|
||||
title: 'Target Project Location',
|
||||
description: `Accepts the format 'gitlab.com?repo=project_name&owner=group_name' where 'project_name' is the repository name and 'group_name' is a group or username`,
|
||||
},
|
||||
sourceAccessToken: {
|
||||
type: 'string',
|
||||
title: 'Source Group Access Token',
|
||||
description: `The token to use for authorization to the source GitLab'`,
|
||||
},
|
||||
sourceFullPath: {
|
||||
type: 'string',
|
||||
title: 'Group Full Path',
|
||||
description:
|
||||
'Full path to the project in the source Gitlab instance',
|
||||
},
|
||||
sourceUrl: {
|
||||
type: 'string',
|
||||
title: 'Source URL Location',
|
||||
description: `Accepts the format 'https://gitlab.com/'`,
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
importedRepoUrl: {
|
||||
title: 'URL to the newly imported repo',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
async handler(ctx) {
|
||||
const {
|
||||
destinationAccessToken,
|
||||
destinationUrl,
|
||||
sourceAccessToken,
|
||||
sourceFullPath,
|
||||
sourceUrl,
|
||||
} = ctx.input;
|
||||
|
||||
const {
|
||||
host: destinationHost,
|
||||
repo: destinationSlug,
|
||||
owner: destinationNamespace,
|
||||
} = parseRepoUrl(destinationUrl, integrations);
|
||||
|
||||
if (!destinationNamespace) {
|
||||
throw new InputError(
|
||||
`Failed to determine target repository to migrate to. Make sure destinationUrl matches the format 'gitlab.myorg.com?repo=project_name&owner=group_name'`,
|
||||
);
|
||||
}
|
||||
|
||||
const api = createGitlabApi({
|
||||
integrations,
|
||||
token: destinationAccessToken,
|
||||
repoUrl: destinationUrl,
|
||||
});
|
||||
|
||||
const migrationEntity: MigrationEntityOptions[] = [
|
||||
{
|
||||
sourceType: 'project_entity',
|
||||
sourceFullPath: sourceFullPath,
|
||||
destinationSlug: destinationSlug,
|
||||
destinationNamespace: destinationNamespace,
|
||||
},
|
||||
];
|
||||
|
||||
const sourceConfig = {
|
||||
url: sourceUrl,
|
||||
access_token: sourceAccessToken,
|
||||
};
|
||||
|
||||
try {
|
||||
await api.Migrations.create(sourceConfig, migrationEntity);
|
||||
} catch (e: any) {
|
||||
throw new InputError(
|
||||
`Failed to transfer repo ${sourceFullPath}. Make sure that ${sourceFullPath} exists in ${sourceUrl}, and token has enough rights.\nError: ${e}`,
|
||||
);
|
||||
}
|
||||
|
||||
ctx.output(
|
||||
'importedRepoUrl',
|
||||
`${destinationHost}/${destinationNamespace}/${destinationSlug}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
export * from './gitlab';
|
||||
export * from './gitlabGroupEnsureExists';
|
||||
export * from './gitlabProjectMigrate';
|
||||
export * from './gitlabIssueCreate';
|
||||
export * from './gitlabIssueEdit';
|
||||
export * from './gitlabMergeRequest';
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '@backstage/plugin-scaffolder-node/alpha';
|
||||
import {
|
||||
createGitlabGroupEnsureExistsAction,
|
||||
createGitlabProjectMigrateAction,
|
||||
createGitlabIssueAction,
|
||||
createGitlabProjectAccessTokenAction,
|
||||
createGitlabProjectDeployTokenAction,
|
||||
@@ -55,6 +56,7 @@ export const gitlabModule = createBackendModule({
|
||||
|
||||
scaffolder.addActions(
|
||||
createGitlabGroupEnsureExistsAction({ integrations }),
|
||||
createGitlabProjectMigrateAction({ integrations }),
|
||||
createGitlabIssueAction({ integrations }),
|
||||
createGitlabProjectAccessTokenAction({ integrations }),
|
||||
createGitlabProjectDeployTokenAction({ integrations }),
|
||||
|
||||
Reference in New Issue
Block a user