@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.
@@ -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 getResourceGroupId(bearer, accountId, resourceGroup) {
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
- getResourceGroupId,
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
+ }
@@ -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
 
@@ -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.\n\nEnter a value: `;
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('\n');
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.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);
@@ -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 (crn) {
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 (value) {
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
  };
@@ -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, TERRAFORMER_REQUIRED_VERSION } 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
 
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('Warning! You have a toolchain with the same name within the target region and resource group! \n', LOG_STAGES.setup, true);
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('(Recommended) Change the cloned toolchain\'s name:\n', tcName, validateToolchainName);
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('Warning! You have a toolchain with the same name within the target region!\n', LOG_STAGES.setup, true);
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('Warning! You have a toolchain with the same name within the target resource group!\n', LOG_STAGES.setup, true);
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 = SECRETS_MAP[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
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 invalid = nonConfiguredTools.length > 0 || patTools.length > 0 || classicPipelines.length > 0 || toolsWithHashedParams.length > 0;
241
+ const hasInvalidConfig = nonConfiguredTools.length > 0 || patTools.length > 0 || toolsWithHashedParams.length > 0;
283
242
 
284
- // Manually fail and reset spinner to prevent duplicate spinners
285
- if (invalid) {
286
- logger.failSpinner('Invalid tools found!');
287
- logger.resetSpinner();
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 && invalid) await promptUserConfirmation('Caution: The above tool(s) will not be properly configured post migration. Do you want to proceed?', 'yes', 'Toolchain migration cancelled.');
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
- logger.print(`${o.type}: \x1b[34m${o.link}\x1b[0m\n`);
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
- 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"
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
  };