@ibm-cloud/cd-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +35 -0
- package/LICENSE +201 -0
- package/README.md +2 -0
- package/cmd/check-secrets.js +106 -0
- package/cmd/copy-toolchain.js +333 -0
- package/cmd/direct-transfer.js +288 -0
- package/cmd/index.js +13 -0
- package/cmd/utils/logger.js +173 -0
- package/cmd/utils/requests.js +359 -0
- package/cmd/utils/terraform.js +441 -0
- package/cmd/utils/utils.js +128 -0
- package/cmd/utils/validate.js +503 -0
- package/config.js +202 -0
- package/index.js +24 -0
- package/package.json +28 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed Materials - Property of IBM
|
|
3
|
+
* (c) Copyright IBM Corporation 2025. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* Note to U.S. Government Users Restricted Rights:
|
|
6
|
+
* Use, duplication or disclosure restricted by GSA ADP Schedule
|
|
7
|
+
* Contract with IBM Corp.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { logger, LOG_STAGES } from './logger.js'
|
|
12
|
+
import { RESERVED_GRIT_PROJECT_NAMES, RESERVED_GRIT_GROUP_NAMES, RESERVED_GRIT_SUBGROUP_NAME, TERRAFORM_REQUIRED_VERSION, TERRAFORMER_REQUIRED_VERSION } from '../../config.js';
|
|
13
|
+
import { getToolchainsByName, getToolchainTools, getPipelineData, getAppConfigHealthcheck, getSecretsHealthcheck, getGitOAuth, getGritUserProject, getGritGroup, getGritGroupProject } from './requests.js';
|
|
14
|
+
import { promptUserConfirmation, promptUserInput } from './utils.js';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const SECRETS_MAP = {
|
|
18
|
+
'artifactory': ['token'],
|
|
19
|
+
'hashicorpvault': ['token', 'role_id', 'secret_id', 'password'],
|
|
20
|
+
'jenkins': ['webhook_url', 'api_token'],
|
|
21
|
+
'jira': ['api_token'],
|
|
22
|
+
'nexus': ['token'],
|
|
23
|
+
'pagerduty': ['service_key'],
|
|
24
|
+
'privateworker': ['worker_queue_credentials'],
|
|
25
|
+
'saucelabs': ['access_key'],
|
|
26
|
+
'securitycompliance': ['scc_api_key'],
|
|
27
|
+
'slack': ['webhook'],
|
|
28
|
+
'sonarqube': ['user_password'],
|
|
29
|
+
'gitlab': ['api_token'],
|
|
30
|
+
'githubconsolidated': ['api_token'],
|
|
31
|
+
'github_integrated': ['api_token']
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
function validatePrereqsVersions() {
|
|
36
|
+
const compareVersions = (verInstalled, verRequired) => {
|
|
37
|
+
const installedSplit = verInstalled.split('.').map(Number);
|
|
38
|
+
const requiredSplit = verRequired.split('.').map(Number);
|
|
39
|
+
for (let j = 0; j < Math.max(installedSplit.length, requiredSplit.length); j++) {
|
|
40
|
+
const i = installedSplit[j] || 0;
|
|
41
|
+
const r = requiredSplit[j] || 0;
|
|
42
|
+
if (i > r) return true;
|
|
43
|
+
if (i < r) return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let stdout;
|
|
49
|
+
let version;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
stdout = execSync('terraform version').toString();
|
|
53
|
+
} catch {
|
|
54
|
+
throw Error('Terraform is not installed or not in PATH');
|
|
55
|
+
}
|
|
56
|
+
version = stdout.match(/\d+(\.\d+)+/)[0];
|
|
57
|
+
if (!compareVersions(version, TERRAFORM_REQUIRED_VERSION)) {
|
|
58
|
+
throw Error(`Terraform does not meet minimum version requirement: ${TERRAFORM_REQUIRED_VERSION}`);
|
|
59
|
+
}
|
|
60
|
+
logger.info(`\x1b[32m✔\x1b[0m Terraform Version: ${version}`, LOG_STAGES.setup);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
stdout = execSync('terraformer version').toString();
|
|
64
|
+
} catch {
|
|
65
|
+
throw Error('Terraformer is not installed or not in PATH');
|
|
66
|
+
}
|
|
67
|
+
version = stdout.match(/\d+(\.\d+)+/)[0];
|
|
68
|
+
if (!compareVersions(version, TERRAFORMER_REQUIRED_VERSION)) {
|
|
69
|
+
throw Error(`Terraformer does not meet minimum version requirement: ${TERRAFORMER_REQUIRED_VERSION}`);
|
|
70
|
+
}
|
|
71
|
+
logger.info(`\x1b[32m✔\x1b[0m Terraformer Version: ${version}`, LOG_STAGES.setup);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateToolchainId(tcId) {
|
|
75
|
+
if (typeof tcId != 'string') throw Error('Provided toolchain ID is not a string');
|
|
76
|
+
const trimmed = tcId.trim();
|
|
77
|
+
|
|
78
|
+
// pattern from api docs
|
|
79
|
+
const pattern = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$/;
|
|
80
|
+
if (pattern.test(trimmed)) {
|
|
81
|
+
return trimmed;
|
|
82
|
+
}
|
|
83
|
+
throw Error('Provided toolchain ID is invalid');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateToolchainName(tcName) {
|
|
87
|
+
if (typeof tcName != 'string') throw Error('Provided toolchain name is not a string');
|
|
88
|
+
const trimmed = tcName.trim();
|
|
89
|
+
|
|
90
|
+
// pattern from api docs
|
|
91
|
+
const pattern = /^([^\x00-\x7F]|[a-zA-Z0-9-._ ])+$/;
|
|
92
|
+
if (trimmed.length <= 128 && pattern.test(trimmed.trim())) {
|
|
93
|
+
return trimmed;
|
|
94
|
+
}
|
|
95
|
+
throw Error('Provided toolchain name is invalid');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validateResourceGroupId(rgId) {
|
|
99
|
+
if (typeof rgId != 'string') throw Error('Provided resource group ID is not a string');
|
|
100
|
+
const trimmed = rgId.trim();
|
|
101
|
+
|
|
102
|
+
// pattern from api docs
|
|
103
|
+
const pattern = /^[0-9a-f]{32}$/;
|
|
104
|
+
if (pattern.test(trimmed)) {
|
|
105
|
+
return trimmed;
|
|
106
|
+
}
|
|
107
|
+
throw Error('Provided resource group ID is invalid');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateTag(tag) {
|
|
111
|
+
if (typeof tag != 'string') throw Error('Provided resource group ID is not a string');
|
|
112
|
+
const trimmed = tag.trim();
|
|
113
|
+
|
|
114
|
+
// only contains alphanumeric characters, spaces, underscores, dashes, periods and colons not at start or end
|
|
115
|
+
const pattern = /^[a-zA-Z0-9-._ ]{1,126}[a-zA-Z0-9-._ :]{1,126}[a-zA-Z0-9-._ ]{1,126}$|^([a-zA-Z0-9-._ ]{1,128})$/;
|
|
116
|
+
if (trimmed.length <= 128 && pattern.test(trimmed.trim())) {
|
|
117
|
+
return trimmed;
|
|
118
|
+
}
|
|
119
|
+
throw Error('Provided tag is invalid');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegion, targetResourceGroupId, targetTag, skipPrompt) {
|
|
124
|
+
const toolchains = await getToolchainsByName(token, accountId, tcName);
|
|
125
|
+
|
|
126
|
+
let hasSameRegion = false;
|
|
127
|
+
let hasSameResourceGroup = false;
|
|
128
|
+
let hasBoth = false;
|
|
129
|
+
|
|
130
|
+
if (toolchains.length > 0) {
|
|
131
|
+
let newTcName = tcName;
|
|
132
|
+
let newTag = targetTag;
|
|
133
|
+
|
|
134
|
+
toolchains.forEach((tc) => {
|
|
135
|
+
if (tc.region_id === targetRegion) {
|
|
136
|
+
if (tc.resource_group_id === targetResourceGroupId) {
|
|
137
|
+
hasBoth = true;
|
|
138
|
+
} else {
|
|
139
|
+
hasSameRegion = true;
|
|
140
|
+
}
|
|
141
|
+
} else if (tc.resource_group_id === targetResourceGroupId) {
|
|
142
|
+
hasSameResourceGroup = true;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (hasBoth) {
|
|
147
|
+
// warning! prompt user to cancel, rename (e.g. add a suffix) or continue
|
|
148
|
+
logger.warn('Warning! You have a toolchain with the same name within the target region and resource group! \n', LOG_STAGES.setup, true);
|
|
149
|
+
|
|
150
|
+
if (!skipPrompt) {
|
|
151
|
+
newTcName = await promptUserInput('(Recommended) Change the cloned toolchain\'s name:\n', tcName, validateToolchainName);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
if (hasSameRegion) {
|
|
155
|
+
// soft warning of confusion
|
|
156
|
+
logger.warn('Warning! You have a toolchain with the same name within the target region!\n', LOG_STAGES.setup, true);
|
|
157
|
+
}
|
|
158
|
+
if (hasSameResourceGroup) {
|
|
159
|
+
// soft warning of confusion
|
|
160
|
+
logger.warn('Warning! You have a toolchain with the same name within the target resource group!\n', LOG_STAGES.setup, true);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (hasBoth || hasSameRegion || hasSameResourceGroup) {
|
|
165
|
+
// suggest a tag, if one not provided
|
|
166
|
+
if (!targetTag) {
|
|
167
|
+
if (!skipPrompt) {
|
|
168
|
+
const validateTagOrEmpty = (str) => {
|
|
169
|
+
if (str.trim() === '') return null;
|
|
170
|
+
return validateTag(str);
|
|
171
|
+
}
|
|
172
|
+
newTag = await promptUserInput('(Recommended) Add a tag to the cloned toolchain:\n', `cloned-from-${srcRegion}`, validateTagOrEmpty);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return [newTcName, newTag];
|
|
176
|
+
} else {
|
|
177
|
+
return [tcName, targetTag];
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
return [tcName, targetTag];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function validateTools(token, tcId, region, skipPrompt) {
|
|
185
|
+
const allTools = await getToolchainTools(token, tcId, region);
|
|
186
|
+
const nonConfiguredTools = [];
|
|
187
|
+
const toolsWithHashedParams = [];
|
|
188
|
+
const patTools = [];
|
|
189
|
+
const classicPipelines = [];
|
|
190
|
+
const secretPattern = /^hash:SHA3-512:[a-zA-Z0-9]{128}$/;
|
|
191
|
+
|
|
192
|
+
for (const tool of allTools.tools) {
|
|
193
|
+
const toolName = (tool.name || tool.parameters?.name || tool.parameters?.label || '').replace(/\s+/g, '+');
|
|
194
|
+
logger.updateSpinnerMsg(`Validating tool \'${toolName}\'`);
|
|
195
|
+
const toolUrl = `https://cloud.ibm.com/devops/toolchains/${tool.toolchain_id}/configure/${tool.id}?env_id=ibm:yp:${region}`;
|
|
196
|
+
|
|
197
|
+
if (tool.state !== 'configured') { // Check for tools in misconfigured/unconfigured/configuring state
|
|
198
|
+
nonConfiguredTools.push({
|
|
199
|
+
tool_name: toolName,
|
|
200
|
+
type: tool.tool_type_id,
|
|
201
|
+
state: tool.state,
|
|
202
|
+
url: toolUrl
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
// handle health check failures, which forces an "error" state in the UI
|
|
206
|
+
if (tool.tool_type_id === 'appconfig') {
|
|
207
|
+
try {
|
|
208
|
+
await getAppConfigHealthcheck(token, tcId, tool.id, region);
|
|
209
|
+
} catch {
|
|
210
|
+
nonConfiguredTools.push({
|
|
211
|
+
tool_name: toolName,
|
|
212
|
+
type: tool.tool_type_id,
|
|
213
|
+
state: 'error',
|
|
214
|
+
url: toolUrl
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} else if (['hashicorpvault', 'secretsmanager', 'keyprotect'].includes(tool.tool_type_id)) {
|
|
218
|
+
try {
|
|
219
|
+
// secrets healthcheck uses parameter name
|
|
220
|
+
const paramName = tool.parameters?.name || '';
|
|
221
|
+
await getSecretsHealthcheck(token, tcId, paramName, region);
|
|
222
|
+
} catch {
|
|
223
|
+
nonConfiguredTools.push({
|
|
224
|
+
tool_name: toolName,
|
|
225
|
+
type: tool.tool_type_id,
|
|
226
|
+
state: 'error',
|
|
227
|
+
url: toolUrl
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (tool.tool_type_id === 'hostedgit' && tool.parameters?.auth_type === 'pat') { // Check for GRIT using PAT
|
|
234
|
+
patTools.push({
|
|
235
|
+
tool_name: toolName,
|
|
236
|
+
type: tool.tool_type_id,
|
|
237
|
+
url: toolUrl
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
else if (tool.tool_type_id === 'pipeline' && tool.parameters?.type === 'classic') { // Check for Classic pipelines
|
|
241
|
+
classicPipelines.push({
|
|
242
|
+
tool_name: toolName,
|
|
243
|
+
type: 'classic pipeline',
|
|
244
|
+
url: toolUrl
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
else if (['githubconsolidated', 'github_integrated', 'gitlab'].includes(tool.tool_type_id) && (tool.parameters?.auth_type === '' || tool.parameters?.auth_type === 'oauth')) { // Skip secret check iff it's GitHub/GitLab integration with OAuth
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const secrets = [];
|
|
252
|
+
if (tool.tool_type_id === 'pipeline' && tool.parameters?.type === 'tekton') { // Check for secrets in Tekton pipeline
|
|
253
|
+
const pipelineData = await getPipelineData(token, tool.id, region);
|
|
254
|
+
|
|
255
|
+
pipelineData.properties.forEach((prop) => {
|
|
256
|
+
if (prop.type === 'secure' && secretPattern.test(prop.value)) secrets.push(['properties', prop.name].join('.').replace(/\s+/g, '+'));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
pipelineData.triggers.forEach((trigger) => {
|
|
260
|
+
if ((trigger?.secret?.type === 'token_matches' || trigger?.secret?.type === 'digest_matches') && secretPattern.test(trigger.secret.value)) secrets.push([trigger.name, trigger.secret.key_name].join('.').replace(/\s+/g, '+'));
|
|
261
|
+
trigger.properties.forEach((prop) => {
|
|
262
|
+
if (prop.type === 'secure' && secretPattern.test(prop.value)) secrets.push([trigger.name, 'properties', prop.name].join('.').replace(/\s+/g, '+'));
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const secretsToCheck = SECRETS_MAP[tool.tool_type_id] || []; // Check for secrets in the rest of the tools
|
|
268
|
+
Object.entries(tool.parameters).forEach(([key, value]) => {
|
|
269
|
+
if (secretPattern.test(value) && secretsToCheck.includes(key)) secrets.push(key);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (secrets.length > 0) {
|
|
273
|
+
toolsWithHashedParams.push({
|
|
274
|
+
tool_name: toolName,
|
|
275
|
+
type: tool.tool_type_id,
|
|
276
|
+
secret_params: secrets,
|
|
277
|
+
url: toolUrl
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const invalid = nonConfiguredTools.length > 0 || patTools.length > 0 || classicPipelines.length > 0 || toolsWithHashedParams.length > 0;
|
|
283
|
+
|
|
284
|
+
// Manually fail and reset spinner to prevent duplicate spinners
|
|
285
|
+
if (invalid) {
|
|
286
|
+
logger.failSpinner('Invalid tools found!');
|
|
287
|
+
logger.resetSpinner();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (nonConfiguredTools.length > 0) {
|
|
291
|
+
logger.warn('Warning! The following tool(s) are not in configured state in toolchain, please reconfigure them before proceeding: \n', LOG_STAGES.setup, true);
|
|
292
|
+
logger.table(nonConfiguredTools);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (patTools.length > 0) {
|
|
296
|
+
logger.warn('Warning! The following GRIT integration(s) are using auth_type "pat", please switch to auth_type "oauth" before proceeding: \n', LOG_STAGES.setup, true);
|
|
297
|
+
logger.table(patTools);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (classicPipelines.length > 0) {
|
|
301
|
+
logger.warn('Warning! Classic pipelines are currently not supported in migration:\n', LOG_STAGES.setup, true);
|
|
302
|
+
logger.table(classicPipelines);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (toolsWithHashedParams.length > 0) {
|
|
306
|
+
logger.warn('Warning! The following tools contain secrets that cannot be migrated, please use the \'check-secret\' command to export the secrets: \n', LOG_STAGES.setup, true);
|
|
307
|
+
logger.table(toolsWithHashedParams);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!skipPrompt && invalid) await promptUserConfirmation('Caution: The above tool(s) will not be properly configured post migration. Do you want to proceed?', 'yes', 'Toolchain migration cancelled.');
|
|
311
|
+
|
|
312
|
+
return allTools.tools;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function validateOAuth(token, tools, targetRegion, skipPrompt) {
|
|
316
|
+
let gitTools = [];
|
|
317
|
+
|
|
318
|
+
for (const tool of tools) {
|
|
319
|
+
const toolName = tool.parameters?.label || tool.parameters?.name || tool.name;
|
|
320
|
+
|
|
321
|
+
let gitId = tool.parameters?.git_id;
|
|
322
|
+
|
|
323
|
+
// the following gets the authorize_url for each source, uniquely identifying GHE and non-GHE repos
|
|
324
|
+
if (tool.tool_type_id === 'githubconsolidated' || tool.tool_type_id === 'github_integrated') {
|
|
325
|
+
// GHE gets converted anyway, so include in GH case
|
|
326
|
+
if (tool.parameters?.auth_type != 'oauth' || !tool.parameters?.git_id) continue;
|
|
327
|
+
tool._sortId = gitId;
|
|
328
|
+
tool._label = toolName;
|
|
329
|
+
gitTools.push(tool);
|
|
330
|
+
} else if (tool.tool_type_id === 'gitlab') {
|
|
331
|
+
if (tool.parameters?.auth_type != 'oauth' || !tool.parameters?.git_id) continue;
|
|
332
|
+
tool._sortId = gitId;
|
|
333
|
+
tool._label = toolName;
|
|
334
|
+
gitTools.push(tool);
|
|
335
|
+
} else if (tool.tool_type_id === 'bitbucketgit') {
|
|
336
|
+
// has no auth_type
|
|
337
|
+
if (!tool.parameters?.git_id) continue;
|
|
338
|
+
tool._sortId = gitId;
|
|
339
|
+
tool._label = toolName;
|
|
340
|
+
gitTools.push(tool);
|
|
341
|
+
} else if (tool.tool_type_id === 'hostedgit') {
|
|
342
|
+
// in GRIT case, getGitOAuth will actually authorize automatically
|
|
343
|
+
if (tool.parameters?.auth_type != 'oauth' || !tool.parameters?.git_id) continue;
|
|
344
|
+
tool._sortId = gitId;
|
|
345
|
+
tool._label = toolName;
|
|
346
|
+
gitTools.push(tool);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// sort gitTools by _sortId = git_id (asc), then name (asc)
|
|
351
|
+
gitTools = gitTools.sort((a, b) => {
|
|
352
|
+
if (a._sortId < b._sortId) {
|
|
353
|
+
return -1;
|
|
354
|
+
} else if (a._sortId > b._sortId) {
|
|
355
|
+
return 1;
|
|
356
|
+
} else {
|
|
357
|
+
if (a._label < b._label) {
|
|
358
|
+
return -1;
|
|
359
|
+
} else if (a._label > b._label) {
|
|
360
|
+
return 1;
|
|
361
|
+
} else {
|
|
362
|
+
return 0
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const failedOAuth = new Set();
|
|
368
|
+
const successfulOAuth = new Set();
|
|
369
|
+
const failedTools = [];
|
|
370
|
+
const oauthLinks = [];
|
|
371
|
+
|
|
372
|
+
for (const tool of gitTools) {
|
|
373
|
+
const isGHE = tool._sortId === 'integrated';
|
|
374
|
+
|
|
375
|
+
if (failedOAuth.has(tool._sortId)) {
|
|
376
|
+
// don't retry failed attempts
|
|
377
|
+
failedTools.push({
|
|
378
|
+
tool_name: tool._label,
|
|
379
|
+
type: tool.tool_type_id + (isGHE ? ' (GHE)' : ''),
|
|
380
|
+
link: '',
|
|
381
|
+
})
|
|
382
|
+
} else if (successfulOAuth.has(tool._sortId)) {
|
|
383
|
+
// don't retry successful attempts
|
|
384
|
+
} else {
|
|
385
|
+
try {
|
|
386
|
+
// attempt to get access token, meaning oauth was set up correctly
|
|
387
|
+
await getGitOAuth(token, targetRegion, tool.parameters?.git_id);
|
|
388
|
+
successfulOAuth.add(tool._sortId);
|
|
389
|
+
} catch (authorizeUrl) {
|
|
390
|
+
failedOAuth.add(tool._sortId);
|
|
391
|
+
failedTools.push({
|
|
392
|
+
tool_name: tool._label,
|
|
393
|
+
type: tool.tool_type_id + (isGHE ? ' (GHE)' : ''),
|
|
394
|
+
link: authorizeUrl?.message != 'Get git OAuth failed' ? 'See link below' : 'Get git OAuth failed',
|
|
395
|
+
})
|
|
396
|
+
if (authorizeUrl?.message != 'Get git OAuth failed') {
|
|
397
|
+
if (isGHE) {
|
|
398
|
+
oauthLinks.push({ type: 'githubconsolidated (GHE)', link: authorizeUrl?.message });
|
|
399
|
+
} else {
|
|
400
|
+
oauthLinks.push({ type: tool.tool_type_id, link: authorizeUrl?.message });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Manually fail and reset spinner to prevent duplicate spinners
|
|
408
|
+
if (failedOAuth.size > 0) {
|
|
409
|
+
logger.failSpinner();
|
|
410
|
+
logger.resetSpinner();
|
|
411
|
+
|
|
412
|
+
logger.warn('Warning! The following git tool integration(s) are not authorized in the target region: \n', LOG_STAGES.setup, true);
|
|
413
|
+
logger.table(failedTools);
|
|
414
|
+
|
|
415
|
+
logger.print('Authorize using the following links: \n');
|
|
416
|
+
oauthLinks.forEach((o) => {
|
|
417
|
+
logger.print(`${o.type}: \x1b[34m${o.link}\x1b[0m\n`);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (!skipPrompt) await promptUserConfirmation('Caution: The above git tool integration(s) will not be properly configured post migration. Do you want to proceed?', 'yes', 'Toolchain migration cancelled.');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function validateGritUrl(token, region, url, validateFull) {
|
|
425
|
+
if (typeof url != 'string') throw Error('Provided GRIT url is not a string');
|
|
426
|
+
let trimmed;
|
|
427
|
+
|
|
428
|
+
if (validateFull) {
|
|
429
|
+
if (!url.startsWith(`https://${region}.git.cloud.ibm.com/`) || !url.endsWith('.git')) throw Error('Provided full GRIT url is not valid');
|
|
430
|
+
trimmed = url.slice(`https://${region}.git.cloud.ibm.com/`.length, url.length - '.git'.length);
|
|
431
|
+
} else {
|
|
432
|
+
trimmed = url.trim();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// split into two parts, user/group/subgroup and project
|
|
436
|
+
const urlSplit = trimmed.split('/');
|
|
437
|
+
if (urlSplit.length < 2) throw Error('Provided GRIT url is invalid (missing forward-slash)');
|
|
438
|
+
|
|
439
|
+
const projectName = urlSplit[urlSplit.length - 1];
|
|
440
|
+
const urlStart = trimmed.slice(0, trimmed.length - projectName.length - 1);
|
|
441
|
+
|
|
442
|
+
// check reserved names, see https://docs.gitlab.com/user/reserved_names/
|
|
443
|
+
if (RESERVED_GRIT_PROJECT_NAMES.includes(projectName)) throw Error('Provided GRIT url invalid (contains reserved name)');
|
|
444
|
+
if (RESERVED_GRIT_GROUP_NAMES.includes(urlStart)) throw Error('Provided GRIT url invalid (contains reserved name)');
|
|
445
|
+
if (urlStart.includes(RESERVED_GRIT_SUBGROUP_NAME)) throw Error('Provided GRIT url invalid (contains reserved name)');
|
|
446
|
+
|
|
447
|
+
// valid characters only, max length 255
|
|
448
|
+
const pattern1 = /^[a-zA-Z0-9-._]{0,255}$/;
|
|
449
|
+
const pattern1alt = /^[a-zA-Z0-9-._\/]{0,255}$/;
|
|
450
|
+
|
|
451
|
+
// starts and ends with alphanumeric
|
|
452
|
+
const pattern2 = /^[a-zA-Z0-9].*$/;
|
|
453
|
+
const pattern3 = /^.*[a-zA-Z0-9]$/;
|
|
454
|
+
|
|
455
|
+
// no consecutive special characters
|
|
456
|
+
const pattern4 = /^.*[-._\/]{2,}.*$/; // want false
|
|
457
|
+
|
|
458
|
+
if (!pattern1.test(projectName)) throw Error('Provided project contains illegal character(s)');
|
|
459
|
+
if (!pattern2.test(projectName)) throw Error('Provided project does not start with an alphanumeric character');
|
|
460
|
+
if (!pattern3.test(projectName)) throw Error('Provided project does not end with an alphanumeric character');
|
|
461
|
+
if (pattern4.test(projectName)) throw Error('Provided project contains consecutive special characters');
|
|
462
|
+
if (!pattern1alt.test(urlStart)) throw Error('Provided user/group contains illegal character(s)');
|
|
463
|
+
if (!pattern2.test(urlStart)) throw Error('Provided user/group does not start with an alphanumeric character');
|
|
464
|
+
if (!pattern3.test(urlStart)) throw Error('Provided user/group does not end with an alphanumeric character');
|
|
465
|
+
if (pattern4.test(urlStart)) throw Error('Provided user/group contains consecutive special characters');
|
|
466
|
+
|
|
467
|
+
// cannot end with .git or .atom
|
|
468
|
+
if (projectName.endsWith('.git') && !projectName.endsWith('.atom')) throw Error('Provided GRIT url contains .git or .atom suffix');
|
|
469
|
+
const accessToken = await getGitOAuth(token, region, 'hostedgit');
|
|
470
|
+
|
|
471
|
+
// validate using API
|
|
472
|
+
let hasFailed = false;
|
|
473
|
+
|
|
474
|
+
// try user
|
|
475
|
+
try {
|
|
476
|
+
await getGritUserProject(accessToken, region, urlStart, projectName);
|
|
477
|
+
} catch {
|
|
478
|
+
hasFailed = true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!hasFailed) return trimmed;
|
|
482
|
+
|
|
483
|
+
// try group
|
|
484
|
+
try {
|
|
485
|
+
const groupId = await getGritGroup(accessToken, region, urlStart);
|
|
486
|
+
await getGritGroupProject(accessToken, region, groupId, projectName);
|
|
487
|
+
return trimmed;
|
|
488
|
+
} catch {
|
|
489
|
+
throw Error('Provided GRIT url not found');
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export {
|
|
494
|
+
validatePrereqsVersions,
|
|
495
|
+
validateToolchainId,
|
|
496
|
+
validateToolchainName,
|
|
497
|
+
validateResourceGroupId,
|
|
498
|
+
validateTag,
|
|
499
|
+
validateTools,
|
|
500
|
+
validateOAuth,
|
|
501
|
+
validateGritUrl,
|
|
502
|
+
warnDuplicateName
|
|
503
|
+
}
|