@ibm-cloud/cd-tools 1.2.5 → 1.3.1

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.
@@ -20,10 +20,10 @@ import { getAccountId, getBearerToken, getCdInstanceByRegion, getIamAuthPolicies
20
20
  import { validatePrereqsVersions, validateTag, validateToolchainId, validateToolchainName, validateTools, validateOAuth, warnDuplicateName, validateGritUrl } from './utils/validate.js';
21
21
  import { importTerraform } from './utils/import-terraform.js';
22
22
 
23
- import { COPY_TOOLCHAIN_DESC, MIGRATION_DOC_URL, TARGET_REGIONS, SOURCE_REGIONS } from '../config.js';
23
+ import { COPY_TOOLCHAIN_DESC, DOCS_URL, TARGET_REGIONS, SOURCE_REGIONS } from '../config.js';
24
24
 
25
25
  process.on('exit', (code) => {
26
- if (code !== 0) logger.print(`Need help? Visit ${MIGRATION_DOC_URL} for more troubleshooting information.`);
26
+ if (code !== 0) logger.print(`Need help? Visit ${DOCS_URL} for more troubleshooting information.`);
27
27
  });
28
28
 
29
29
  const TIME_SUFFIX = new Date().getTime();
@@ -45,21 +45,24 @@ const command = new Command('copy-toolchain')
45
45
  .choices(TARGET_REGIONS)
46
46
  .makeOptionMandatory()
47
47
  )
48
- .option('-a --apikey <api key>', 'API Key used to perform the copy. Must have IAM permission to read and create toolchains and S2S authorizations in source and target region / resource group')
48
+ .option('-a, --apikey <api_key>', 'API key used to authenticate. Must have IAM permission to read and create toolchains and service-to-service authorizations in source and target region / resource group')
49
49
  .option('-n, --name <name>', '(Optional) The name of the copied toolchain (default: same name as original)')
50
- .option('-g, --resource-group <resource group>', '(Optional) The name or ID of destination resource group of the copied toolchain (default: same resource group as original)')
50
+ .option('-g, --resource-group <resource_group>', '(Optional) The name or ID of destination resource group of the copied toolchain (default: same resource group as original)')
51
51
  .option('-t, --tag <tag>', '(Optional) The tag to add to the copied toolchain')
52
52
  .helpOption('-h, --help', 'Display help for command')
53
53
  .optionsGroup('Advanced options:')
54
- .option('-d, --terraform-dir <directory path>', '(Optional) The target local directory to store the generated Terraform (.tf) files')
55
- .option('-D, --dry-run', '(Optional) Skip running terraform apply')
54
+ .option('-d, --terraform-dir <path>', '(Optional) The target local directory to store the generated Terraform (.tf) files')
55
+ .option('-D, --dry-run', '(Optional) Skip running terraform apply; only generate the Terraform (.tf) files')
56
56
  .option('-f, --force', '(Optional) Force the copy toolchain command to run without user confirmation')
57
- .option('-S, --skip-s2s', '(Optional) Skip importing toolchain-generated S2S authorizations')
58
- .option('-T, --skip-disable-triggers', '(Optional) Skip disabling triggers')
59
- .option('-C, --compact', '(Optional) Generate all resources in resources.tf')
60
- .option('-v --verbose', '(Optional) Increase log output')
61
- .option('-s --silent', '(Optional) Suppress non-essential output, only errors and critical warnings are displayed')
62
- .option('-G --grit-mapping-file <path>', '(Optional) JSON file mapping GRIT project urls to project urls in the target region')
57
+ .option('-S, --skip-s2s', '(Optional) Skip importing toolchain-generated service-to-service authorizations')
58
+ .option('-T, --skip-disable-triggers', '(Optional) Skip disabling Tekton pipeline Git or timed triggers. Note: This may result in duplicate pipeline runs')
59
+ .option('-C, --compact', '(Optional) Generate all resources in a single resources.tf file')
60
+ .option('-v, --verbose', '(Optional) Increase log output')
61
+ .option('-q, --quiet', '(Optional) Suppress non-essential output, only errors and critical warnings are displayed')
62
+ .addOption(
63
+ new Option('-G, --grit-mapping-file <path>', '(Optional) JSON file mapping GRIT project urls to project urls in the target region')
64
+ .hideHelp()
65
+ )
63
66
  .showHelpAfterError()
64
67
  .hook('preAction', cmd => cmd.showHelpAfterError(false)) // only show help during validation
65
68
  .action(main);
@@ -74,7 +77,7 @@ async function main(options) {
74
77
  const includeS2S = !options.skipS2s;
75
78
  const disableTriggers = !options.skipDisableTriggers;
76
79
  const isCompact = options.compact || false;
77
- const verbosity = options.silent ? 0 : options.verbose ? 2 : 1;
80
+ const verbosity = options.quiet ? 0 : options.verbose ? 2 : 1;
78
81
 
79
82
  logger.setVerbosity(verbosity);
80
83
  if (LOG_DUMP) logger.createLogStream(`${LOGS_DIR}/copy-toolchain-${new Date().getTime()}.log`);
@@ -302,8 +302,8 @@ async function getGitOAuth(bearer, targetRegion, gitId) {
302
302
  switch (response.status) {
303
303
  case 200:
304
304
  return response.data?.access_token;
305
- case 500:
306
- throw Error(response.data?.authorizationURI);
305
+ case 401:
306
+ throw Error(response.data?.authorizationURI ?? 'Get git OAuth failed');
307
307
  default:
308
308
  throw Error('Get git OAuth failed');
309
309
  }
@@ -332,14 +332,13 @@ async function getGritUserProject(privToken, region, user, projectName) {
332
332
  }
333
333
 
334
334
  async function getGritGroup(privToken, region, groupName) {
335
- const url = `https://${region}.git.cloud.ibm.com/api/v4/groups`
335
+ const url = `https://${region}.git.cloud.ibm.com/api/v4/groups/${groupName}`
336
336
  const options = {
337
337
  url: url,
338
338
  method: 'GET',
339
339
  headers: {
340
340
  'PRIVATE-TOKEN': privToken
341
341
  },
342
- params: { simple: true, search: groupName },
343
342
  validateStatus: () => true
344
343
  };
345
344
  const response = await axios(options);
@@ -146,6 +146,13 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
146
146
  if (isCompact || resourceName === 'ibm_cd_toolchain_tool_hostedgit') {
147
147
  if (newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit']) {
148
148
  for (const [k, v] of Object.entries(newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit'])) {
149
+ try {
150
+ newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit'][k]['parameters'][0]['auth_type'] = 'oauth';
151
+ delete newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit'][k]['parameters'][0]['api_token'];
152
+ } catch {
153
+ // do nothing
154
+ }
155
+
149
156
  try {
150
157
  const thisUrl = v['initialization'][0]['repo_url'];
151
158
  if (thisUrl in gritMapping) {
@@ -163,6 +170,10 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
163
170
  } else {
164
171
  // prompt user
165
172
  const validateGritUrlPrompt = async (str) => {
173
+ if (!str) {
174
+ logger.print('Skipping... (URL will remain unchanged in the generatedTerraform configuration)');
175
+ return '';
176
+ }
166
177
  const newUrl = `https://${targetRegion}.git.cloud.ibm.com/${str}.git`;
167
178
  if (usedGritUrls.has(newUrl)) throw Error(`"${newUrl}" has already been used in another mapping entry`);
168
179
  return validateGritUrl(token, targetRegion, str, false);
@@ -170,15 +181,17 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
170
181
 
171
182
  if (!firstGritPrompt) {
172
183
  firstGritPrompt = true;
173
- logger.print('Please enter the new URLs for the following GRIT tool(s):\n');
184
+ logger.print('Please enter the new URLs for the following GRIT tool(s) (or submit empty input to skip):\n');
174
185
  }
175
186
 
176
187
  const newRepoSlug = await promptUserInput(`Old URL: ${thisUrl.slice(0, thisUrl.length - 4)}\nNew URL: https://${targetRegion}.git.cloud.ibm.com/`, '', validateGritUrlPrompt);
177
188
 
178
- newUrl = `https://${targetRegion}.git.cloud.ibm.com/${newRepoSlug}.git`;
179
- newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit'][k]['initialization'][0]['repo_url'] = newUrl;
180
- attemptAddUsedGritUrl(newUrl);
181
- gritMapping[thisUrl] = newUrl;
189
+ if (newRepoSlug) {
190
+ newUrl = `https://${targetRegion}.git.cloud.ibm.com/${newRepoSlug}.git`;
191
+ newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit'][k]['initialization'][0]['repo_url'] = newUrl;
192
+ attemptAddUsedGritUrl(newUrl);
193
+ gritMapping[thisUrl] = newUrl;
194
+ }
182
195
  }
183
196
  }
184
197
  catch (e) {
@@ -145,6 +145,7 @@ async function validateTools(token, tcId, region, skipPrompt) {
145
145
  const nonConfiguredTools = [];
146
146
  const toolsWithHashedParams = [];
147
147
  const patTools = [];
148
+ const gheTools = [];
148
149
  const classicPipelines = [];
149
150
 
150
151
  for (const tool of allTools.tools) {
@@ -195,14 +196,24 @@ async function validateTools(token, tcId, region, skipPrompt) {
195
196
  url: toolUrl
196
197
  });
197
198
  }
198
- else if (tool.tool_type_id === 'pipeline' && tool.parameters?.type === 'classic') { // Check for Classic pipelines
199
+
200
+ if (tool.tool_type_id === 'github_integrated') {
201
+ gheTools.push({
202
+ tool_name: toolName,
203
+ type: tool.tool_type_id,
204
+ url: toolUrl
205
+ });
206
+ }
207
+
208
+ if (tool.tool_type_id === 'pipeline' && tool.parameters?.type === 'classic') { // Check for Classic pipelines
199
209
  classicPipelines.push({
200
210
  tool_name: toolName,
201
211
  type: 'classic pipeline',
202
212
  url: toolUrl
203
213
  });
204
214
  }
205
- else if (['githubconsolidated', 'github_integrated', 'gitlab', 'hostedgit'].includes(tool.tool_type_id) && (tool.parameters?.auth_type === '' || tool.parameters?.auth_type === 'oauth')) { // Skip secret check iff it's GitHub/GitLab/GRIT integration with OAuth
215
+
216
+ if (['githubconsolidated', 'github_integrated', 'gitlab', 'hostedgit'].includes(tool.tool_type_id) && (tool.parameters?.auth_type === '' || tool.parameters?.auth_type === 'oauth')) { // Skip secret check iff it's GitHub/GitLab/GRIT integration with OAuth
206
217
  continue;
207
218
  }
208
219
  else {
@@ -240,7 +251,7 @@ async function validateTools(token, tcId, region, skipPrompt) {
240
251
  }
241
252
  }
242
253
  }
243
- const hasInvalidConfig = nonConfiguredTools.length > 0 || patTools.length > 0 || toolsWithHashedParams.length > 0;
254
+ const hasInvalidConfig = nonConfiguredTools.length > 0 || toolsWithHashedParams.length > 0;
244
255
 
245
256
  if (classicPipelines.length > 0) {
246
257
  logger.failSpinner('Unsupported tools found!');
@@ -257,10 +268,15 @@ async function validateTools(token, tcId, region, skipPrompt) {
257
268
  }
258
269
 
259
270
  if (patTools.length > 0) {
260
- logger.warn('Warning! The following GRIT integration(s) are using auth_type "pat", please switch to auth_type "oauth" before proceeding: \n', LOG_STAGES.setup, true);
271
+ logger.warn('Warning! The following GRIT integration(s) with auth_type "pat" are unsupported during migration and will automatically be converted to auth_type "oauth": \n', LOG_STAGES.setup, true);
261
272
  logger.table(patTools);
262
273
  }
263
274
 
275
+ if (gheTools.length > 0) {
276
+ logger.warn('Warning! The following legacy GHE integration(s) are unsupported during migration will automatically be converted to equivalent GitHub integrations: \n', LOG_STAGES.setup, true);
277
+ logger.table(gheTools);
278
+ }
279
+
264
280
  if (toolsWithHashedParams.length > 0) {
265
281
  logger.warn('Warning! The following tools contain secrets that cannot be migrated, please use the \'check-secrets\' command to export the secrets: \n', LOG_STAGES.setup, true);
266
282
  logger.table(toolsWithHashedParams);
@@ -353,12 +369,10 @@ async function validateOAuth(token, tools, targetRegion, skipPrompt) {
353
369
  type: tool.tool_type_id + (isGHE ? ' (GHE)' : ''),
354
370
  link: authorizeUrl?.message != 'Get git OAuth failed' ? 'See link below' : 'Get git OAuth failed',
355
371
  })
356
- if (authorizeUrl?.message != 'Get git OAuth failed') {
357
- if (isGHE) {
358
- oauthLinks.push({ type: 'githubconsolidated (GHE)', link: authorizeUrl?.message });
359
- } else {
360
- oauthLinks.push({ type: tool.tool_type_id, link: authorizeUrl?.message ?? 'Could not get OAuth link' });
361
- }
372
+ if (isGHE) {
373
+ oauthLinks.push({ type: 'githubconsolidated (GHE)', link: authorizeUrl?.message });
374
+ } else {
375
+ oauthLinks.push({ type: tool.tool_type_id, link: authorizeUrl?.message });
362
376
  }
363
377
  }
364
378
  }
@@ -376,7 +390,7 @@ async function validateOAuth(token, tools, targetRegion, skipPrompt) {
376
390
 
377
391
  if (oauthLinks.length > 0) logger.print('Authorize using the following links:\n');
378
392
  oauthLinks.forEach((o) => {
379
- if (o.link === 'Could not get OAuth link') hasFailedLink = true;
393
+ if (o.link === 'Get git OAuth failed') hasFailedLink = true;
380
394
  logger.print(`${o.type}: \x1b[36m${o.link}\x1b[0m\n`);
381
395
  });
382
396
 
@@ -447,8 +461,7 @@ async function validateGritUrl(token, region, url, validateFull) {
447
461
 
448
462
  // try group
449
463
  try {
450
- const groupId = await getGritGroup(accessToken, region, urlStart);
451
- await getGritGroupProject(accessToken, region, groupId, projectName);
464
+ await getGritGroupProject(accessToken, region, urlStart, projectName);
452
465
  return trimmed;
453
466
  } catch {
454
467
  throw Error('Provided GRIT url not found');
package/config.js CHANGED
@@ -17,9 +17,9 @@ Examples:
17
17
  Copy a toolchain to the Frankfurt region with the specified name and target resource group, using the given API key
18
18
 
19
19
  Environment Variables:
20
- IBMCLOUD_API_KEY API Key used to perform the copy. Must have IAM permission to read and create toolchains and S2S authorizations in source and target region / resource group`
20
+ IBMCLOUD_API_KEY API key used to authenticate. Must have IAM permission to read and create toolchains and service-to-service authorizations in source and target region / resource group`
21
21
 
22
- const MIGRATION_DOC_URL = 'https://github.com/IBM/continuous-delivery-tools'; // TODO: replace with docs link
22
+ const DOCS_URL = 'https://github.com/IBM/continuous-delivery-tools';
23
23
 
24
24
  const SOURCE_REGIONS = [
25
25
  'au-syd',
@@ -220,7 +220,7 @@ const VAULT_REGEX = [
220
220
 
221
221
  export {
222
222
  COPY_TOOLCHAIN_DESC,
223
- MIGRATION_DOC_URL,
223
+ DOCS_URL,
224
224
  SOURCE_REGIONS,
225
225
  TARGET_REGIONS,
226
226
  TERRAFORM_REQUIRED_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibm-cloud/cd-tools",
3
- "version": "1.2.5",
3
+ "version": "1.3.1",
4
4
  "description": "Tools and utilities for the IBM Cloud Continuous Delivery service and resources",
5
5
  "repository": {
6
6
  "type": "git",
@@ -119,8 +119,8 @@ describe('copy-toolchain: Test functionalities', function () {
119
119
  }
120
120
  },
121
121
  {
122
- name: 'Silent flag suppresses info, debug and log messages',
123
- cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region, '-s'],
122
+ name: 'Quiet flag suppresses info, debug and log messages',
123
+ cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region, '-q'],
124
124
  expected: null,
125
125
  options: {
126
126
  timeout: 100000,