@ibm-cloud/cd-tools 1.1.1 → 1.2.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/README.md +4 -4
- package/cmd/check-secrets.js +13 -12
- package/cmd/copy-toolchain.js +48 -25
- package/cmd/utils/import-terraform.js +333 -0
- package/cmd/utils/logger.js +9 -6
- package/cmd/utils/requests.js +51 -6
- package/cmd/utils/terraform.js +12 -33
- package/cmd/utils/utils.js +31 -6
- package/cmd/utils/validate.js +25 -63
- package/config.js +60 -29
- package/package.json +11 -2
- package/test/README.md +40 -0
- package/test/config/local.template.json +8 -0
- package/test/copy-toolchain/import.test.js +11 -0
- package/test/copy-toolchain/terraform.test.js +11 -0
- package/test/copy-toolchain/validation.test.js +126 -0
- package/test/data/mocks.js +29 -0
- package/test/data/test-toolchains.js +21 -0
- package/test/setup.js +46 -0
- package/test/utils/testUtils.js +137 -0
package/cmd/utils/requests.js
CHANGED
|
@@ -185,7 +185,7 @@ async function getPipelineData(bearer, pipelineId, region) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// takes in resource group ID or name
|
|
188
|
-
async function
|
|
188
|
+
async function getResourceGroupIdAndName(bearer, accountId, resourceGroup) {
|
|
189
189
|
const apiBaseUrl = 'https://api.global-search-tagging.cloud.ibm.com/v3';
|
|
190
190
|
const options = {
|
|
191
191
|
url: apiBaseUrl + '/resources/search',
|
|
@@ -196,7 +196,7 @@ async function getResourceGroupId(bearer, accountId, resourceGroup) {
|
|
|
196
196
|
},
|
|
197
197
|
data: {
|
|
198
198
|
'query': `type:resource-group AND (name:${resourceGroup} OR doc.id:${resourceGroup}) AND doc.state:ACTIVE`,
|
|
199
|
-
'fields': ['doc.id']
|
|
199
|
+
'fields': ['doc.id', 'doc.name']
|
|
200
200
|
},
|
|
201
201
|
params: { account_id: accountId },
|
|
202
202
|
validateStatus: () => true
|
|
@@ -205,7 +205,7 @@ async function getResourceGroupId(bearer, accountId, resourceGroup) {
|
|
|
205
205
|
switch (response.status) {
|
|
206
206
|
case 200:
|
|
207
207
|
if (response.data.items.length != 1) throw Error('The resource group with provided ID or name was not found or is not accessible');
|
|
208
|
-
return response.data.items[0].doc.id;
|
|
208
|
+
return { id: response.data.items[0].doc.id, name: response.data.items[0].doc.name };
|
|
209
209
|
default:
|
|
210
210
|
throw Error('The resource group with provided ID or name was not found or is not accessible');
|
|
211
211
|
}
|
|
@@ -320,6 +320,7 @@ async function getGritGroup(privToken, region, groupName) {
|
|
|
320
320
|
throw Error('Get GRIT group failed');
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
|
+
|
|
323
324
|
async function getGritGroupProject(privToken, region, groupId, projectName) {
|
|
324
325
|
const url = `https://${region}.git.cloud.ibm.com/api/v4/groups/${groupId}/projects`
|
|
325
326
|
const options = {
|
|
@@ -342,6 +343,48 @@ async function getGritGroupProject(privToken, region, groupId, projectName) {
|
|
|
342
343
|
}
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
async function getIamAuthPolicies(bearer, accountId) {
|
|
347
|
+
const apiBaseUrl = 'https://iam.cloud.ibm.com/v1';
|
|
348
|
+
const options = {
|
|
349
|
+
url: apiBaseUrl + '/policies',
|
|
350
|
+
method: 'GET',
|
|
351
|
+
headers: {
|
|
352
|
+
'Authorization': `Bearer ${bearer}`,
|
|
353
|
+
'Content-Type': 'application/json',
|
|
354
|
+
},
|
|
355
|
+
params: { account_id: accountId, type: 'authorization' },
|
|
356
|
+
validateStatus: () => true
|
|
357
|
+
};
|
|
358
|
+
const response = await axios(options);
|
|
359
|
+
switch (response.status) {
|
|
360
|
+
case 200:
|
|
361
|
+
return response.data;
|
|
362
|
+
default:
|
|
363
|
+
throw Error('Get auth policies failed');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function deleteToolchain(bearer, toolchainId, region) {
|
|
368
|
+
const apiBaseUrl = `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
|
|
369
|
+
const options = {
|
|
370
|
+
method: 'DELETE',
|
|
371
|
+
url: `${apiBaseUrl}/toolchains/${toolchainId}`,
|
|
372
|
+
headers: {
|
|
373
|
+
'Accept': 'application/json',
|
|
374
|
+
'Authorization': `Bearer ${bearer}`,
|
|
375
|
+
'Content-Type': 'application/json',
|
|
376
|
+
},
|
|
377
|
+
validateStatus: () => true
|
|
378
|
+
};
|
|
379
|
+
const response = await axios(options);
|
|
380
|
+
switch (response.status) {
|
|
381
|
+
case 200:
|
|
382
|
+
return toolchainId;
|
|
383
|
+
default:
|
|
384
|
+
throw Error(response.statusText);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
345
388
|
export {
|
|
346
389
|
getBearerToken,
|
|
347
390
|
getAccountId,
|
|
@@ -349,11 +392,13 @@ export {
|
|
|
349
392
|
getToolchainsByName,
|
|
350
393
|
getToolchainTools,
|
|
351
394
|
getPipelineData,
|
|
352
|
-
|
|
395
|
+
getResourceGroupIdAndName,
|
|
353
396
|
getAppConfigHealthcheck,
|
|
354
397
|
getSecretsHealthcheck,
|
|
355
398
|
getGitOAuth,
|
|
356
399
|
getGritUserProject,
|
|
357
400
|
getGritGroup,
|
|
358
|
-
getGritGroupProject
|
|
359
|
-
|
|
401
|
+
getGritGroupProject,
|
|
402
|
+
getIamAuthPolicies,
|
|
403
|
+
deleteToolchain
|
|
404
|
+
}
|
package/cmd/utils/terraform.js
CHANGED
|
@@ -17,7 +17,7 @@ import { jsonToTf } from 'json-to-tf';
|
|
|
17
17
|
|
|
18
18
|
import { validateToolchainId, validateGritUrl } from './validate.js';
|
|
19
19
|
import { logger } from './logger.js';
|
|
20
|
-
import { promptUserInput, replaceUrlRegion } from './utils.js';
|
|
20
|
+
import { getRandChars, promptUserInput, replaceUrlRegion } from './utils.js';
|
|
21
21
|
|
|
22
22
|
// promisify
|
|
23
23
|
const readFilePromise = promisify(fs.readFile);
|
|
@@ -34,15 +34,9 @@ async function execPromise(command, options) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function
|
|
38
|
-
process.env['IC_API_KEY'] = apiKey;
|
|
39
|
-
process.env['IBM_CD_TOOLCHAIN_TARGET'] = tcId;
|
|
40
|
-
if (includeS2S) process.env['IBM_CD_TOOLCHAIN_INCLUDE_S2S'] = 1;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function setTerraformEnv(verbosity) {
|
|
37
|
+
function setTerraformEnv(apiKey, verbosity) {
|
|
44
38
|
if (verbosity >= 2) process.env['TF_LOG'] = 'DEBUG';
|
|
45
|
-
process.env['TF_VAR_ibmcloud_api_key'] =
|
|
39
|
+
process.env['TF_VAR_ibmcloud_api_key'] = apiKey;
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
async function initProviderFile(targetRegion, dir) {
|
|
@@ -54,13 +48,7 @@ async function initProviderFile(targetRegion, dir) {
|
|
|
54
48
|
|
|
55
49
|
const newProviderTfStr = JSON.stringify(newProviderTf)
|
|
56
50
|
|
|
57
|
-
return writeFilePromise(`${dir}/provider.tf`, jsonToTf(newProviderTfStr)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function runTerraformerImport(srcRegion, tempDir, isCompact, verbosity) {
|
|
61
|
-
const stdout = await execPromise(`terraformer import ibm --resources=ibm_cd_toolchain --region=${srcRegion} -S ${isCompact ? '--compact' : ''} ${verbosity >= 2 ? '--verbose' : ''}`, { cwd: tempDir });
|
|
62
|
-
if (verbosity >= 2) logger.print(stdout);
|
|
63
|
-
return stdout;
|
|
51
|
+
return writeFilePromise(`${dir}/provider.tf`, jsonToTf(newProviderTfStr));
|
|
64
52
|
}
|
|
65
53
|
|
|
66
54
|
async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag, targetToolchainName, targetRgId, disableTriggers, isCompact, outputDir, tempDir, moreTfResources, gritMapping, skipUserConfirmation }) {
|
|
@@ -71,7 +59,7 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
|
|
|
71
59
|
|
|
72
60
|
// Get toolchain resource
|
|
73
61
|
const toolchainLocation = isCompact ? 'resources.tf' : 'cd_toolchain.tf'
|
|
74
|
-
const resources = await readFilePromise(`${tempDir}/generated
|
|
62
|
+
const resources = await readFilePromise(`${tempDir}/generated/${toolchainLocation}`, 'utf8');
|
|
75
63
|
const resourcesObj = await tfToJson('output.tf', resources);
|
|
76
64
|
const newTcId = Object.keys(resourcesObj['resource']['ibm_cd_toolchain'])[0];
|
|
77
65
|
|
|
@@ -84,7 +72,7 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
|
|
|
84
72
|
promises.push(writeOutputPromise);
|
|
85
73
|
|
|
86
74
|
// Copy over cd_*.tf
|
|
87
|
-
let files = await readDirPromise(`${tempDir}/generated
|
|
75
|
+
let files = await readDirPromise(`${tempDir}/generated`);
|
|
88
76
|
|
|
89
77
|
if (isCompact) {
|
|
90
78
|
files = files.filter((f) => f === 'resources.tf');
|
|
@@ -108,17 +96,6 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
|
|
|
108
96
|
const newConvertedTf = {};
|
|
109
97
|
|
|
110
98
|
if (hasGHE) {
|
|
111
|
-
const getRandChars = (size) => {
|
|
112
|
-
const charSet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
113
|
-
let res = '';
|
|
114
|
-
|
|
115
|
-
for (let i = 0; i < size; i++) {
|
|
116
|
-
const pos = randomInt(charSet.length);
|
|
117
|
-
res += charSet[pos];
|
|
118
|
-
}
|
|
119
|
-
return res;
|
|
120
|
-
};
|
|
121
|
-
|
|
122
99
|
moreTfResources['github_integrated'].forEach(t => {
|
|
123
100
|
const gitUrl = t['parameters']['repo_url'];
|
|
124
101
|
const tfName = `converted--githubconsolidated_${getRandChars(4)}`;
|
|
@@ -146,7 +123,7 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
|
|
|
146
123
|
}
|
|
147
124
|
|
|
148
125
|
for (const fileName of files) {
|
|
149
|
-
const tfFile = await readFilePromise(`${tempDir}/generated
|
|
126
|
+
const tfFile = await readFilePromise(`${tempDir}/generated/${fileName}`, 'utf8');
|
|
150
127
|
const tfFileObj = await tfToJson(fileName, tfFile);
|
|
151
128
|
|
|
152
129
|
const newTfFileObj = { 'resource': {} }
|
|
@@ -203,7 +180,6 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
|
|
|
203
180
|
newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit'][k]['initialization'][0]['repo_url'] = newUrl;
|
|
204
181
|
attemptAddUsedGritUrl(newUrl);
|
|
205
182
|
gritMapping[thisUrl] = newUrl;
|
|
206
|
-
logger.print('\n');
|
|
207
183
|
}
|
|
208
184
|
}
|
|
209
185
|
catch (e) {
|
|
@@ -325,6 +301,10 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
|
|
|
325
301
|
return Promise.all(promises);
|
|
326
302
|
}
|
|
327
303
|
|
|
304
|
+
async function runTerraformPlanGenerate(dir, fileName) {
|
|
305
|
+
return await execPromise(`terraform plan -generate-config-out=${fileName}`, { cwd: dir });
|
|
306
|
+
}
|
|
307
|
+
|
|
328
308
|
async function runTerraformInit(dir) {
|
|
329
309
|
return await execPromise('terraform init', { cwd: dir });
|
|
330
310
|
}
|
|
@@ -428,12 +408,11 @@ function replaceDependsOn(str) {
|
|
|
428
408
|
}
|
|
429
409
|
|
|
430
410
|
export {
|
|
431
|
-
setTerraformerEnv,
|
|
432
411
|
setTerraformEnv,
|
|
433
412
|
initProviderFile,
|
|
434
|
-
runTerraformerImport,
|
|
435
413
|
setupTerraformFiles,
|
|
436
414
|
runTerraformInit,
|
|
415
|
+
runTerraformPlanGenerate,
|
|
437
416
|
getNumResourcesPlanned,
|
|
438
417
|
runTerraformApply,
|
|
439
418
|
getNewToolchainId,
|
package/cmd/utils/utils.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as readline from 'node:readline/promises';
|
|
11
|
+
import { randomInt } from 'node:crypto';
|
|
12
|
+
|
|
11
13
|
import { logger } from './logger.js';
|
|
12
14
|
import { VAULT_REGEX } from '../../config.js';
|
|
13
15
|
|
|
@@ -25,7 +27,7 @@ export async function promptUserConfirmation(question, expectedAns, exitMsg) {
|
|
|
25
27
|
output: process.stdout
|
|
26
28
|
});
|
|
27
29
|
|
|
28
|
-
const fullPrompt = question + `\n\nOnly '${expectedAns}' will be accepted to proceed
|
|
30
|
+
const fullPrompt = question + `\n\nOnly '${expectedAns}' will be accepted to proceed. (Ctrl-C to abort)\n\nEnter a value: `;
|
|
29
31
|
const answer = await rl.question(fullPrompt);
|
|
30
32
|
|
|
31
33
|
logger.dump(fullPrompt + '\n' + answer + '\n');
|
|
@@ -38,7 +40,7 @@ export async function promptUserConfirmation(question, expectedAns, exitMsg) {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
rl.close();
|
|
41
|
-
logger.print(
|
|
43
|
+
logger.print();
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export async function promptUserInput(question, initialInput, validationFn) {
|
|
@@ -67,7 +69,7 @@ export async function promptUserInput(question, initialInput, validationFn) {
|
|
|
67
69
|
break;
|
|
68
70
|
} catch (e) {
|
|
69
71
|
// loop
|
|
70
|
-
logger.
|
|
72
|
+
logger.warn(`Validation failed... ${e.message}`, '', true);
|
|
71
73
|
|
|
72
74
|
rl.prompt(true);
|
|
73
75
|
rl.write(initialInput);
|
|
@@ -75,6 +77,7 @@ export async function promptUserInput(question, initialInput, validationFn) {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
rl.close();
|
|
80
|
+
logger.print();
|
|
78
81
|
return answer.trim();
|
|
79
82
|
}
|
|
80
83
|
|
|
@@ -97,12 +100,12 @@ export function replaceUrlRegion(inputUrl, srcRegion, targetRegion) {
|
|
|
97
100
|
*
|
|
98
101
|
* @param {String} crn - The crn to decompose.
|
|
99
102
|
**/
|
|
100
|
-
export function decomposeCrn
|
|
103
|
+
export function decomposeCrn(crn) {
|
|
101
104
|
const crnParts = crn.split(':');
|
|
102
105
|
|
|
103
106
|
// Remove the 'a/' segment.
|
|
104
107
|
let accountId = crnParts[6];
|
|
105
|
-
if(accountId) {
|
|
108
|
+
if (accountId) {
|
|
106
109
|
accountId = accountId.split('/')[1];
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -123,6 +126,28 @@ export function decomposeCrn (crn) {
|
|
|
123
126
|
*
|
|
124
127
|
* @param {String} value - The value to verify.
|
|
125
128
|
**/
|
|
126
|
-
export function isSecretReference
|
|
129
|
+
export function isSecretReference(value) {
|
|
127
130
|
return !!(VAULT_REGEX.find(r => r.test(value)));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function getRandChars(size) {
|
|
134
|
+
const charSet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
135
|
+
let res = '';
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < size; i++) {
|
|
138
|
+
const pos = randomInt(charSet.length);
|
|
139
|
+
res += charSet[pos];
|
|
140
|
+
}
|
|
141
|
+
return res;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export function normalizeName(str) {
|
|
145
|
+
const specialChars = `-<>()*#{}[]|@_ .%'",&`;
|
|
146
|
+
let newStr = str;
|
|
147
|
+
|
|
148
|
+
for (const char of specialChars) {
|
|
149
|
+
newStr = newStr.replaceAll(char, '_');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return newStr.toLowerCase();
|
|
128
153
|
};
|
package/cmd/utils/validate.js
CHANGED
|
@@ -9,29 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
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,
|
|
12
|
+
import { RESERVED_GRIT_PROJECT_NAMES, RESERVED_GRIT_GROUP_NAMES, RESERVED_GRIT_SUBGROUP_NAME, TERRAFORM_REQUIRED_VERSION, SECRET_KEYS_MAP } from '../../config.js';
|
|
13
13
|
import { getToolchainsByName, getToolchainTools, getPipelineData, getAppConfigHealthcheck, getSecretsHealthcheck, getGitOAuth, getGritUserProject, getGritGroup, getGritGroupProject } from './requests.js';
|
|
14
14
|
import { promptUserConfirmation, promptUserInput } from './utils.js';
|
|
15
15
|
|
|
16
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
17
|
function validatePrereqsVersions() {
|
|
36
18
|
const compareVersions = (verInstalled, verRequired) => {
|
|
37
19
|
const installedSplit = verInstalled.split('.').map(Number);
|
|
@@ -58,17 +40,6 @@ function validatePrereqsVersions() {
|
|
|
58
40
|
throw Error(`Terraform does not meet minimum version requirement: ${TERRAFORM_REQUIRED_VERSION}`);
|
|
59
41
|
}
|
|
60
42
|
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
43
|
}
|
|
73
44
|
|
|
74
45
|
function validateToolchainId(tcId) {
|
|
@@ -95,18 +66,6 @@ function validateToolchainName(tcName) {
|
|
|
95
66
|
throw Error('Provided toolchain name is invalid');
|
|
96
67
|
}
|
|
97
68
|
|
|
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
69
|
function validateTag(tag) {
|
|
111
70
|
if (typeof tag != 'string') throw Error('Provided resource group ID is not a string');
|
|
112
71
|
const trimmed = tag.trim();
|
|
@@ -120,7 +79,7 @@ function validateTag(tag) {
|
|
|
120
79
|
}
|
|
121
80
|
|
|
122
81
|
|
|
123
|
-
async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegion, targetResourceGroupId, targetTag, skipPrompt) {
|
|
82
|
+
async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegion, targetResourceGroupId, targetResourceGroupName, targetTag, skipPrompt) {
|
|
124
83
|
const toolchains = await getToolchainsByName(token, accountId, tcName);
|
|
125
84
|
|
|
126
85
|
let hasSameRegion = false;
|
|
@@ -145,19 +104,19 @@ async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegi
|
|
|
145
104
|
|
|
146
105
|
if (hasBoth) {
|
|
147
106
|
// warning! prompt user to cancel, rename (e.g. add a suffix) or continue
|
|
148
|
-
logger.warn(
|
|
107
|
+
logger.warn(`\nWarning! A toolchain named '${tcName}' already exists in:\n - Region: ${targetRegion}\n - Resource Group: ${targetResourceGroupName} (${targetResourceGroupId})`, '', true);
|
|
149
108
|
|
|
150
109
|
if (!skipPrompt) {
|
|
151
|
-
newTcName = await promptUserInput(
|
|
110
|
+
newTcName = await promptUserInput(`\n(Recommended) Edit the cloned toolchain's name [default: ${tcName}] (Ctrl-C to abort):\n`, tcName, validateToolchainName);
|
|
152
111
|
}
|
|
153
112
|
} else {
|
|
154
113
|
if (hasSameRegion) {
|
|
155
114
|
// soft warning of confusion
|
|
156
|
-
logger.warn(
|
|
115
|
+
logger.warn(`\nWarning! A toolchain named '${tcName}' already exists in:\n - Region: ${targetRegion}`, '', true);
|
|
157
116
|
}
|
|
158
117
|
if (hasSameResourceGroup) {
|
|
159
118
|
// soft warning of confusion
|
|
160
|
-
logger.warn(
|
|
119
|
+
logger.warn(`\nWarning! A toolchain named '${tcName}' already exists in:\n - Resource Group: ${targetResourceGroupName} (${targetResourceGroupId})`, '', true);
|
|
161
120
|
}
|
|
162
121
|
}
|
|
163
122
|
|
|
@@ -169,7 +128,7 @@ async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegi
|
|
|
169
128
|
if (str.trim() === '') return null;
|
|
170
129
|
return validateTag(str);
|
|
171
130
|
}
|
|
172
|
-
newTag = await promptUserInput('(Recommended) Add a tag to the cloned toolchain:\n', `cloned-from-${srcRegion}`, validateTagOrEmpty);
|
|
131
|
+
newTag = await promptUserInput('\n(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):\n', `cloned-from-${srcRegion}`, validateTagOrEmpty);
|
|
173
132
|
}
|
|
174
133
|
}
|
|
175
134
|
return [newTcName, newTag];
|
|
@@ -264,7 +223,7 @@ async function validateTools(token, tcId, region, skipPrompt) {
|
|
|
264
223
|
});
|
|
265
224
|
}
|
|
266
225
|
else {
|
|
267
|
-
const secretsToCheck =
|
|
226
|
+
const secretsToCheck = (SECRET_KEYS_MAP[tool.tool_type_id] || []).map((entry) => entry.key); // Check for secrets in the rest of the tools
|
|
268
227
|
Object.entries(tool.parameters).forEach(([key, value]) => {
|
|
269
228
|
if (secretPattern.test(value) && secretsToCheck.includes(key)) secrets.push(key);
|
|
270
229
|
});
|
|
@@ -279,14 +238,17 @@ async function validateTools(token, tcId, region, skipPrompt) {
|
|
|
279
238
|
}
|
|
280
239
|
}
|
|
281
240
|
}
|
|
282
|
-
const
|
|
241
|
+
const hasInvalidConfig = nonConfiguredTools.length > 0 || patTools.length > 0 || toolsWithHashedParams.length > 0;
|
|
283
242
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
logger.
|
|
287
|
-
logger.
|
|
243
|
+
if (classicPipelines.length > 0) {
|
|
244
|
+
logger.failSpinner('Unsupported tools found!');
|
|
245
|
+
logger.warn('Warning! Classic pipelines are currently not supported in migration:\n', LOG_STAGES.setup, true);
|
|
246
|
+
logger.table(classicPipelines);
|
|
288
247
|
}
|
|
289
248
|
|
|
249
|
+
if (hasInvalidConfig) logger.failSpinner('Configuration problems found!');
|
|
250
|
+
if (classicPipelines.length > 0 || hasInvalidConfig) logger.resetSpinner(); // Manually reset spinner to prevent duplicate spinners
|
|
251
|
+
|
|
290
252
|
if (nonConfiguredTools.length > 0) {
|
|
291
253
|
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
254
|
logger.table(nonConfiguredTools);
|
|
@@ -297,17 +259,13 @@ async function validateTools(token, tcId, region, skipPrompt) {
|
|
|
297
259
|
logger.table(patTools);
|
|
298
260
|
}
|
|
299
261
|
|
|
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
262
|
if (toolsWithHashedParams.length > 0) {
|
|
306
263
|
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
264
|
logger.table(toolsWithHashedParams);
|
|
308
265
|
}
|
|
309
266
|
|
|
310
|
-
if (!skipPrompt &&
|
|
267
|
+
if (!skipPrompt && (classicPipelines.length > 0 || hasInvalidConfig))
|
|
268
|
+
await promptUserConfirmation('Caution: The above tool(s) will not be properly configured post migration. Do you want to proceed?', 'yes', 'Toolchain migration cancelled.');
|
|
311
269
|
|
|
312
270
|
return allTools.tools;
|
|
313
271
|
}
|
|
@@ -397,7 +355,7 @@ async function validateOAuth(token, tools, targetRegion, skipPrompt) {
|
|
|
397
355
|
if (isGHE) {
|
|
398
356
|
oauthLinks.push({ type: 'githubconsolidated (GHE)', link: authorizeUrl?.message });
|
|
399
357
|
} else {
|
|
400
|
-
oauthLinks.push({ type: tool.tool_type_id, link: authorizeUrl?.message });
|
|
358
|
+
oauthLinks.push({ type: tool.tool_type_id, link: authorizeUrl?.message ?? 'Could not get OAuth link' });
|
|
401
359
|
}
|
|
402
360
|
}
|
|
403
361
|
}
|
|
@@ -412,11 +370,16 @@ async function validateOAuth(token, tools, targetRegion, skipPrompt) {
|
|
|
412
370
|
logger.warn('Warning! The following git tool integration(s) are not authorized in the target region: \n', LOG_STAGES.setup, true);
|
|
413
371
|
logger.table(failedTools);
|
|
414
372
|
|
|
373
|
+
let hasFailedLink = false;
|
|
374
|
+
|
|
415
375
|
logger.print('Authorize using the following links: \n');
|
|
416
376
|
oauthLinks.forEach((o) => {
|
|
417
|
-
|
|
377
|
+
if (o.link === 'Could not get OAuth link') hasFailedLink = true;
|
|
378
|
+
logger.print(`${o.type}: \x1b[36m${o.link}\x1b[0m\n`);
|
|
418
379
|
});
|
|
419
380
|
|
|
381
|
+
if (hasFailedLink) logger.print(`Please manually verify failed authorization(s): https://cloud.ibm.com/devops/git?env_id=ibm:yp:${targetRegion}\n`);
|
|
382
|
+
|
|
420
383
|
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
384
|
}
|
|
422
385
|
}
|
|
@@ -494,7 +457,6 @@ export {
|
|
|
494
457
|
validatePrereqsVersions,
|
|
495
458
|
validateToolchainId,
|
|
496
459
|
validateToolchainName,
|
|
497
|
-
validateResourceGroupId,
|
|
498
460
|
validateTag,
|
|
499
461
|
validateTools,
|
|
500
462
|
validateOAuth,
|
package/config.js
CHANGED
|
@@ -51,8 +51,6 @@ const TARGET_REGIONS = [
|
|
|
51
51
|
|
|
52
52
|
const TERRAFORM_REQUIRED_VERSION = '1.13.3';
|
|
53
53
|
|
|
54
|
-
const TERRAFORMER_REQUIRED_VERSION = '0.8.30';
|
|
55
|
-
|
|
56
54
|
// see https://docs.gitlab.com/user/reserved_names/
|
|
57
55
|
const RESERVED_GRIT_PROJECT_NAMES = [
|
|
58
56
|
'\\-',
|
|
@@ -122,65 +120,98 @@ const RESERVED_GRIT_GROUP_NAMES = [
|
|
|
122
120
|
|
|
123
121
|
const RESERVED_GRIT_SUBGROUP_NAME = '\\-';
|
|
124
122
|
|
|
125
|
-
|
|
123
|
+
/*
|
|
124
|
+
Format:
|
|
125
|
+
Maps tool_type_id to a list of the following ...
|
|
126
|
+
{
|
|
127
|
+
key: str, // tool parameter key
|
|
128
|
+
tfKey?: str, // terraform-equivalent key
|
|
129
|
+
prereq?: { key: string, values: [string] }, // proceed only if tool parameter "prereq.key" is one of "values"
|
|
130
|
+
required?: bool // is this key required for terraform?
|
|
131
|
+
}
|
|
132
|
+
... which represents a secret/sensitive value
|
|
133
|
+
*/
|
|
134
|
+
const SECRET_KEYS_MAP = {
|
|
126
135
|
"artifactory": [
|
|
127
|
-
"token"
|
|
136
|
+
{ key: "token", tfKey: "token" }
|
|
128
137
|
],
|
|
129
138
|
"cloudobjectstorage": [
|
|
130
|
-
"cos_api_key",
|
|
131
|
-
"hmac_access_key_id",
|
|
132
|
-
"hmac_secret_access_key"
|
|
139
|
+
{ key: "cos_api_key", tfKey: "cos_api_key", prereq: { key: "auth_type", values: ["apikey"] } },
|
|
140
|
+
{ key: "hmac_access_key_id", tfKey: "hmac_access_key_id", prereq: { key: "auth_type", values: ["hmac"] } },
|
|
141
|
+
{ key: "hmac_secret_access_key", tfKey: "hmac_secret_access_key", prereq: { key: "auth_type", values: ["hmac"] } },
|
|
133
142
|
],
|
|
134
143
|
"github_integrated": [
|
|
135
|
-
"api_token"
|
|
144
|
+
{ key: "api_token" } // no terraform equivalent
|
|
136
145
|
],
|
|
137
146
|
"githubconsolidated": [
|
|
138
|
-
"api_token"
|
|
147
|
+
{ key: "api_token", tfKey: "api_token", prereq: { key: "auth_type", values: ["pat"] } },
|
|
139
148
|
],
|
|
140
149
|
"gitlab": [
|
|
141
|
-
"api_token"
|
|
150
|
+
{ key: "api_token", tfKey: "api_token", prereq: { key: "auth_type", values: ["pat"] } },
|
|
142
151
|
],
|
|
143
152
|
"hashicorpvault": [
|
|
144
|
-
"token",
|
|
145
|
-
"role_id",
|
|
146
|
-
"secret_id",
|
|
147
|
-
"password"
|
|
153
|
+
{ key: "token", tfKey: "token", prereq: { key: "authentication_method", values: ["github", "token"] } },
|
|
154
|
+
{ key: "role_id", tfKey: "role_id", prereq: { key: "authentication_method", values: ["approle"] } },
|
|
155
|
+
{ key: "secret_id", tfKey: "secret_id", prereq: { key: "authentication_method", values: ["approle"] } },
|
|
156
|
+
{ key: "password", tfKey: "password", prereq: { key: "authentication_method", values: ["userpass"] } },
|
|
148
157
|
],
|
|
149
158
|
"hostedgit": [
|
|
150
|
-
"api_token"
|
|
159
|
+
{ key: "api_token", tfKey: "api_token", prereq: { key: "auth_type", values: ["pat"] } },
|
|
151
160
|
],
|
|
152
161
|
"jenkins": [
|
|
153
|
-
"api_token"
|
|
162
|
+
{ key: "api_token", tfKey: "api_token" },
|
|
154
163
|
],
|
|
155
164
|
"jira": [
|
|
156
|
-
"password",
|
|
157
|
-
"api_token"
|
|
165
|
+
{ key: "password", tfKey: "api_token" },
|
|
158
166
|
],
|
|
159
167
|
"nexus": [
|
|
160
|
-
"token"
|
|
168
|
+
{ key: "token", tfKey: "token" },
|
|
161
169
|
],
|
|
162
170
|
"pagerduty": [
|
|
163
|
-
"service_key"
|
|
171
|
+
{ key: "service_key", tfKey: "service_key", required: true },
|
|
164
172
|
],
|
|
165
173
|
"private_worker": [
|
|
166
|
-
"workerQueueCredentials"
|
|
174
|
+
{ key: "workerQueueCredentials", tfKey: "worker_queue_credentials", required: true },
|
|
167
175
|
],
|
|
168
176
|
"saucelabs": [
|
|
169
|
-
"key",
|
|
170
|
-
"access_key"
|
|
177
|
+
{ key: "key", tfKey: "access_key", required: true },
|
|
171
178
|
],
|
|
172
179
|
"security_compliance": [
|
|
173
|
-
"scc_api_key"
|
|
180
|
+
{ key: "scc_api_key", tfKey: "scc_api_key", prereq: { key: "use_profile_attachment", values: ["enabled"] } },
|
|
174
181
|
],
|
|
175
182
|
"slack": [
|
|
176
|
-
"api_token",
|
|
177
|
-
"webhook"
|
|
183
|
+
{ key: "api_token", tfKey: "webhook", required: true },
|
|
178
184
|
],
|
|
179
185
|
"sonarqube": [
|
|
180
|
-
"user_password"
|
|
186
|
+
{ key: "user_password", tfKey: "user_password" },
|
|
181
187
|
]
|
|
182
188
|
};
|
|
183
189
|
|
|
190
|
+
// maps tool parameter tool_type_id to terraform resource type
|
|
191
|
+
const SUPPORTED_TOOLS_MAP = {
|
|
192
|
+
"appconfig": "ibm_cd_toolchain_tool_appconfig",
|
|
193
|
+
"artifactory": "ibm_cd_toolchain_tool_artifactory",
|
|
194
|
+
"bitbucketgit": "ibm_cd_toolchain_tool_bitbucketgit",
|
|
195
|
+
"private_worker": "ibm_cd_toolchain_tool_privateworker",
|
|
196
|
+
"draservicebroker": "ibm_cd_toolchain_tool_devopsinsights",
|
|
197
|
+
"eventnotifications": "ibm_cd_toolchain_tool_eventnotifications",
|
|
198
|
+
"hostedgit": "ibm_cd_toolchain_tool_hostedgit",
|
|
199
|
+
"githubconsolidated": "ibm_cd_toolchain_tool_githubconsolidated",
|
|
200
|
+
"gitlab": "ibm_cd_toolchain_tool_gitlab",
|
|
201
|
+
"hashicorpvault": "ibm_cd_toolchain_tool_hashicorpvault",
|
|
202
|
+
"jenkins": "ibm_cd_toolchain_tool_jenkins",
|
|
203
|
+
"jira": "ibm_cd_toolchain_tool_jira",
|
|
204
|
+
"keyprotect": "ibm_cd_toolchain_tool_keyprotect",
|
|
205
|
+
"nexus": "ibm_cd_toolchain_tool_nexus",
|
|
206
|
+
"customtool": "ibm_cd_toolchain_tool_custom",
|
|
207
|
+
"saucelabs": "ibm_cd_toolchain_tool_saucelabs",
|
|
208
|
+
"secretsmanager": "ibm_cd_toolchain_tool_secretsmanager",
|
|
209
|
+
"security_compliance": "ibm_cd_toolchain_tool_securitycompliance",
|
|
210
|
+
"slack": "ibm_cd_toolchain_tool_slack",
|
|
211
|
+
"sonarqube": "ibm_cd_toolchain_tool_sonarqube",
|
|
212
|
+
"pipeline": "ibm_cd_toolchain_tool_pipeline"
|
|
213
|
+
};
|
|
214
|
+
|
|
184
215
|
const VAULT_REGEX = [
|
|
185
216
|
new RegExp('[\\{]{1}(?<reference>\\b(?<provider>vault)\\b[:]{2}(?<integration>[ a-zA-Z0-9_-]*)[.]{0,1}(?<secret>.*))[\\}]{1}', 'iu'),
|
|
186
217
|
new RegExp('^(?<reference>crn:v1:(?:bluemix|staging):public:(?<type>secrets-manager):(?<region>[a-zA-Z0-9-]*)\\b:a\/(?<account_id>[0-9a-fA-F]*)\\b:(?<instance_id>[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12})\\b:secret:(?<secret_id>[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}))$', 'iu'),
|
|
@@ -189,14 +220,14 @@ const VAULT_REGEX = [
|
|
|
189
220
|
|
|
190
221
|
export {
|
|
191
222
|
COPY_TOOLCHAIN_DESC,
|
|
192
|
-
UPDATEABLE_SECRET_PROPERTIES_BY_TOOL_TYPE,
|
|
193
223
|
MIGRATION_DOC_URL,
|
|
194
224
|
SOURCE_REGIONS,
|
|
195
225
|
TARGET_REGIONS,
|
|
196
226
|
TERRAFORM_REQUIRED_VERSION,
|
|
197
|
-
TERRAFORMER_REQUIRED_VERSION,
|
|
198
227
|
RESERVED_GRIT_PROJECT_NAMES,
|
|
199
228
|
RESERVED_GRIT_GROUP_NAMES,
|
|
200
229
|
RESERVED_GRIT_SUBGROUP_NAME,
|
|
230
|
+
SECRET_KEYS_MAP,
|
|
231
|
+
SUPPORTED_TOOLS_MAP,
|
|
201
232
|
VAULT_REGEX
|
|
202
233
|
};
|