@ibm-cloud/cd-tools 1.1.2 → 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 +44 -16
- package/cmd/utils/import-terraform.js +333 -0
- package/cmd/utils/logger.js +9 -6
- package/cmd/utils/requests.js +46 -1
- package/cmd/utils/terraform.js +12 -33
- package/cmd/utils/utils.js +25 -1
- package/cmd/utils/validate.js +8 -14
- package/config.js +60 -30
- 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
|
@@ -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,
|
|
@@ -355,5 +398,7 @@ export {
|
|
|
355
398
|
getGitOAuth,
|
|
356
399
|
getGritUserProject,
|
|
357
400
|
getGritGroup,
|
|
358
|
-
getGritGroupProject
|
|
401
|
+
getGritGroupProject,
|
|
402
|
+
getIamAuthPolicies,
|
|
403
|
+
deleteToolchain
|
|
359
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
|
|
|
@@ -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);
|
|
@@ -127,3 +129,25 @@ export function decomposeCrn(crn) {
|
|
|
127
129
|
export function isSecretReference(value) {
|
|
128
130
|
return !!(VAULT_REGEX.find(r => r.test(value)));
|
|
129
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();
|
|
153
|
+
};
|
package/cmd/utils/validate.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
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
|
|
|
@@ -40,17 +40,6 @@ function validatePrereqsVersions() {
|
|
|
40
40
|
throw Error(`Terraform does not meet minimum version requirement: ${TERRAFORM_REQUIRED_VERSION}`);
|
|
41
41
|
}
|
|
42
42
|
logger.info(`\x1b[32m✔\x1b[0m Terraform Version: ${version}`, LOG_STAGES.setup);
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
stdout = execSync('terraformer version').toString();
|
|
46
|
-
} catch {
|
|
47
|
-
throw Error('Terraformer is not installed or not in PATH');
|
|
48
|
-
}
|
|
49
|
-
version = stdout.match(/\d+(\.\d+)+/)[0];
|
|
50
|
-
if (!compareVersions(version, TERRAFORMER_REQUIRED_VERSION)) {
|
|
51
|
-
throw Error(`Terraformer does not meet minimum version requirement: ${TERRAFORMER_REQUIRED_VERSION}`);
|
|
52
|
-
}
|
|
53
|
-
logger.info(`\x1b[32m✔\x1b[0m Terraformer Version: ${version}`, LOG_STAGES.setup);
|
|
54
43
|
}
|
|
55
44
|
|
|
56
45
|
function validateToolchainId(tcId) {
|
|
@@ -234,7 +223,7 @@ async function validateTools(token, tcId, region, skipPrompt) {
|
|
|
234
223
|
});
|
|
235
224
|
}
|
|
236
225
|
else {
|
|
237
|
-
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
|
|
238
227
|
Object.entries(tool.parameters).forEach(([key, value]) => {
|
|
239
228
|
if (secretPattern.test(value) && secretsToCheck.includes(key)) secrets.push(key);
|
|
240
229
|
});
|
|
@@ -366,7 +355,7 @@ async function validateOAuth(token, tools, targetRegion, skipPrompt) {
|
|
|
366
355
|
if (isGHE) {
|
|
367
356
|
oauthLinks.push({ type: 'githubconsolidated (GHE)', link: authorizeUrl?.message });
|
|
368
357
|
} else {
|
|
369
|
-
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' });
|
|
370
359
|
}
|
|
371
360
|
}
|
|
372
361
|
}
|
|
@@ -381,11 +370,16 @@ async function validateOAuth(token, tools, targetRegion, skipPrompt) {
|
|
|
381
370
|
logger.warn('Warning! The following git tool integration(s) are not authorized in the target region: \n', LOG_STAGES.setup, true);
|
|
382
371
|
logger.table(failedTools);
|
|
383
372
|
|
|
373
|
+
let hasFailedLink = false;
|
|
374
|
+
|
|
384
375
|
logger.print('Authorize using the following links: \n');
|
|
385
376
|
oauthLinks.forEach((o) => {
|
|
377
|
+
if (o.link === 'Could not get OAuth link') hasFailedLink = true;
|
|
386
378
|
logger.print(`${o.type}: \x1b[36m${o.link}\x1b[0m\n`);
|
|
387
379
|
});
|
|
388
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
|
+
|
|
389
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.');
|
|
390
384
|
}
|
|
391
385
|
}
|
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,66 +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",
|
|
167
|
-
"worker_queue_credentials"
|
|
174
|
+
{ key: "workerQueueCredentials", tfKey: "worker_queue_credentials", required: true },
|
|
168
175
|
],
|
|
169
176
|
"saucelabs": [
|
|
170
|
-
"key",
|
|
171
|
-
"access_key"
|
|
177
|
+
{ key: "key", tfKey: "access_key", required: true },
|
|
172
178
|
],
|
|
173
179
|
"security_compliance": [
|
|
174
|
-
"scc_api_key"
|
|
180
|
+
{ key: "scc_api_key", tfKey: "scc_api_key", prereq: { key: "use_profile_attachment", values: ["enabled"] } },
|
|
175
181
|
],
|
|
176
182
|
"slack": [
|
|
177
|
-
"api_token",
|
|
178
|
-
"webhook"
|
|
183
|
+
{ key: "api_token", tfKey: "webhook", required: true },
|
|
179
184
|
],
|
|
180
185
|
"sonarqube": [
|
|
181
|
-
"user_password"
|
|
186
|
+
{ key: "user_password", tfKey: "user_password" },
|
|
182
187
|
]
|
|
183
188
|
};
|
|
184
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
|
+
|
|
185
215
|
const VAULT_REGEX = [
|
|
186
216
|
new RegExp('[\\{]{1}(?<reference>\\b(?<provider>vault)\\b[:]{2}(?<integration>[ a-zA-Z0-9_-]*)[.]{0,1}(?<secret>.*))[\\}]{1}', 'iu'),
|
|
187
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'),
|
|
@@ -190,14 +220,14 @@ const VAULT_REGEX = [
|
|
|
190
220
|
|
|
191
221
|
export {
|
|
192
222
|
COPY_TOOLCHAIN_DESC,
|
|
193
|
-
UPDATEABLE_SECRET_PROPERTIES_BY_TOOL_TYPE,
|
|
194
223
|
MIGRATION_DOC_URL,
|
|
195
224
|
SOURCE_REGIONS,
|
|
196
225
|
TARGET_REGIONS,
|
|
197
226
|
TERRAFORM_REQUIRED_VERSION,
|
|
198
|
-
TERRAFORMER_REQUIRED_VERSION,
|
|
199
227
|
RESERVED_GRIT_PROJECT_NAMES,
|
|
200
228
|
RESERVED_GRIT_GROUP_NAMES,
|
|
201
229
|
RESERVED_GRIT_SUBGROUP_NAME,
|
|
230
|
+
SECRET_KEYS_MAP,
|
|
231
|
+
SUPPORTED_TOOLS_MAP,
|
|
202
232
|
VAULT_REGEX
|
|
203
233
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibm-cloud/cd-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Tools and utilities for the IBM Cloud Continuous Delivery service and resources",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"author": "IBM Corp.",
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "mocha --require \"test/setup.js\" \"test/copy-toolchain/*.test.js\""
|
|
19
|
+
},
|
|
17
20
|
"dependencies": {
|
|
18
21
|
"@cdktf/hcl2json": "^0.21.0",
|
|
19
22
|
"axios": "^1.12.2",
|
|
@@ -27,5 +30,11 @@
|
|
|
27
30
|
"bin": {
|
|
28
31
|
"cd-tools": "index.js"
|
|
29
32
|
},
|
|
30
|
-
"type": "module"
|
|
33
|
+
"type": "module",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"chai": "^6.2.0",
|
|
36
|
+
"mocha": "^11.7.4",
|
|
37
|
+
"nconf": "^0.13.0",
|
|
38
|
+
"node-pty": "^1.0.0"
|
|
39
|
+
}
|
|
31
40
|
}
|
package/test/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Test
|
|
2
|
+
|
|
3
|
+
All automated tests live in this directory.
|
|
4
|
+
Before running tests, ensure that you have completed the setup steps in the main [README.md](../README.md), including installing all prerequisites and dependencies.
|
|
5
|
+
|
|
6
|
+
## Getting Started
|
|
7
|
+
1. **Clone the repository**
|
|
8
|
+
```bash
|
|
9
|
+
git clone https://github.com/IBM/continuous-delivery-tools.git
|
|
10
|
+
cd continuous-delivery-tools
|
|
11
|
+
```
|
|
12
|
+
2. **Install dependencies**
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
3. **Test configuration**
|
|
17
|
+
|
|
18
|
+
Before running tests, create a local configuration file:
|
|
19
|
+
```bash
|
|
20
|
+
cp test/config/local.template.json test/config/local.json
|
|
21
|
+
```
|
|
22
|
+
Then open `test/config/local.json` and replace all placeholder values with your local or test environment settings.
|
|
23
|
+
4. **Running the tests**
|
|
24
|
+
|
|
25
|
+
To execute the test suite:
|
|
26
|
+
```bash
|
|
27
|
+
npm run test
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Test Configuration
|
|
31
|
+
You can customize the behavior of the tests by defining configuration properties in the `test/config/local.json` file.
|
|
32
|
+
|
|
33
|
+
| Property | Type | Default | Description |
|
|
34
|
+
| ------------------ | --------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `TEST_DEBUG_MODE` | `boolean` | `false` | When set to `true`, files generated by test cases in `TEST_TEMP_DIR` and log files generated in `TEST_LOG_DIR` are preserved |
|
|
36
|
+
| `TEST_TEMP_DIR` | `string` | `test/.tmp` | The directory to store temporary files generated by test cases |
|
|
37
|
+
| `TEST_LOG_DIR` | `string` | `test/.logs` | The directory to store test run log files |
|
|
38
|
+
| `IBMCLOUD_API_KEY` | `string` | `null` | The IBM Cloud API Key used to run the tests |
|
|
39
|
+
| `LOG_DUMP` | `boolean` | `false` | When set to `true`, individual test case's process's log file generation is enabled |
|
|
40
|
+
| `DISABLE_SPINNER` | `boolean` | `true` | When set to `true`, visual spinner is disabled across all test cases' processes |
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
describe('copy-toolchain: Test import-terraform output', function () {
|
|
11
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
describe('copy-toolchain: Test Terraform output', function () {
|
|
11
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
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 path from 'node:path';
|
|
11
|
+
import nconf from 'nconf';
|
|
12
|
+
|
|
13
|
+
import * as chai from 'chai';
|
|
14
|
+
chai.config.truncateThreshold = 0;
|
|
15
|
+
|
|
16
|
+
import mocks from '../data/mocks.js';
|
|
17
|
+
import { testSuiteCleanup, expectExecError, expectPtyOutputToMatch } from '../utils/testUtils.js';
|
|
18
|
+
import { TEST_TOOLCHAINS } from '../data/test-toolchains.js';
|
|
19
|
+
import { TARGET_REGIONS } from '../../config.js';
|
|
20
|
+
|
|
21
|
+
nconf.env('__');
|
|
22
|
+
nconf.file('local', 'test/config/local.json');
|
|
23
|
+
|
|
24
|
+
const CLI_PATH = path.resolve('index.js');
|
|
25
|
+
const COMMAND = 'copy-toolchain';
|
|
26
|
+
|
|
27
|
+
const toolchainsToDelete = new Map();
|
|
28
|
+
|
|
29
|
+
after(async () => await testSuiteCleanup(toolchainsToDelete));
|
|
30
|
+
|
|
31
|
+
describe('copy-toolchain: Test user input handling', function () {
|
|
32
|
+
this.timeout('60s');
|
|
33
|
+
this.command = 'copy-toolchain';
|
|
34
|
+
|
|
35
|
+
const validCrn = TEST_TOOLCHAINS['empty'].crn;
|
|
36
|
+
|
|
37
|
+
const invalidArgsCases = [
|
|
38
|
+
{
|
|
39
|
+
name: 'Toolchain CRN not specified',
|
|
40
|
+
cmd: [CLI_PATH, COMMAND],
|
|
41
|
+
expected: /required option '-c, --toolchain-crn <crn>' not specified/,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Region is not specified',
|
|
45
|
+
cmd: [CLI_PATH, COMMAND, '-c', validCrn],
|
|
46
|
+
expected: /required option '-r, --region <region>' not specified/,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'API Key is not specified',
|
|
50
|
+
cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0]],
|
|
51
|
+
expected: /Environment variable 'IBMCLOUD_API_KEY' is required but not set/,
|
|
52
|
+
options: { env: { ...process.env, IBMCLOUD_API_KEY: '' } }
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Invalid API Key provided',
|
|
56
|
+
cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0]],
|
|
57
|
+
expected: /There was a problem getting a bearer token using IBMCLOUD_API_KEY/,
|
|
58
|
+
options: { env: { ...process.env, IBMCLOUD_API_KEY: 'not-a-valid-apikey' } }
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'Invalid region is provided',
|
|
62
|
+
cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', mocks.invalidRegion],
|
|
63
|
+
expected: new RegExp(`option '-r, --region <region>' argument '${mocks.invalidRegion}' is invalid`)
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'Invalid CRN is provided',
|
|
67
|
+
cmd: [CLI_PATH, COMMAND, '-c', mocks.invalidCrn, '-r', TARGET_REGIONS[0]],
|
|
68
|
+
expected: /Provided toolchain CRN is invalid/,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'Invalid Toolchain tag is provided',
|
|
72
|
+
cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-t', mocks.invalidTag],
|
|
73
|
+
expected: /Provided tag is invalid/,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'Invalid Toolchain name is provided',
|
|
77
|
+
cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-n', mocks.invalidTcName],
|
|
78
|
+
expected: /Provided toolchain name is invalid/,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'Invalid Resource Group name is provided',
|
|
82
|
+
cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-g', mocks.invalidRgName],
|
|
83
|
+
expected: /The resource group with provided ID or name was not found or is not accessible/,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'Invalid Resource Group ID is provided',
|
|
87
|
+
cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-g', mocks.invalidRgId],
|
|
88
|
+
expected: /The resource group with provided ID or name was not found or is not accessible/,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const { name, cmd, expected, options } of invalidArgsCases) {
|
|
93
|
+
it(`Invalid args: ${name}`, async () => {
|
|
94
|
+
await expectExecError(cmd, expected, options);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const invalidUserInputCases = [
|
|
99
|
+
{
|
|
100
|
+
name: 'Invalid Toolchain tag is provided',
|
|
101
|
+
cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TARGET_REGIONS[0]],
|
|
102
|
+
expected: /Provided tag is invalid/,
|
|
103
|
+
options: {
|
|
104
|
+
questionAnswerMap: { '(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):' : mocks.invalidTag },
|
|
105
|
+
exitCondition: 'Validation failed',
|
|
106
|
+
timeout: 5000
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'Invalid Toolchain name is provided',
|
|
111
|
+
cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region],
|
|
112
|
+
expected: /Provided toolchain name is invalid/,
|
|
113
|
+
options: {
|
|
114
|
+
questionAnswerMap: { [`(Recommended) Edit the cloned toolchain's name [default: ${TEST_TOOLCHAINS['empty'].name}] (Ctrl-C to abort):`] : mocks.invalidTcName },
|
|
115
|
+
exitCondition: 'Validation failed',
|
|
116
|
+
timeout: 5000
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const { name, cmd, expected, options } of invalidUserInputCases) {
|
|
122
|
+
it(`Invalid user input in prompts: ${name}`, async () => {
|
|
123
|
+
await expectPtyOutputToMatch(cmd, expected, options);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|