@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.
@@ -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
  }
@@ -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 setTerraformerEnv(apiKey, tcId, includeS2S) {
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'] = process.env.IC_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/ibm/ibm_cd_toolchain/${toolchainLocation}`, 'utf8');
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/ibm/ibm_cd_toolchain`);
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/ibm/ibm_cd_toolchain/${fileName}`, 'utf8');
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,
@@ -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.print('Validation failed...', e.message, '\n');
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
+ };
@@ -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, TERRAFORMER_REQUIRED_VERSION, UPDATEABLE_SECRET_PROPERTIES_BY_TOOL_TYPE } from '../../config.js';
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 = UPDATEABLE_SECRET_PROPERTIES_BY_TOOL_TYPE[tool.tool_type_id] || []; // Check for secrets in the rest of the tools
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
- const UPDATEABLE_SECRET_PROPERTIES_BY_TOOL_TYPE = {
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.1.2",
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,8 @@
1
+ {
2
+ "TEST_DEBUG_MODE": false,
3
+ "TEST_TEMP_DIR": "test/.tmp",
4
+ "TEST_LOG_DIR": "test/.logs",
5
+ "IBMCLOUD_API_KEY": "<YOUR IBMCLOUD API KEY>",
6
+ "LOG_DUMP": true,
7
+ "DISABLE_SPINNER": true
8
+ }
@@ -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
+ });