@ibm-cloud/cd-tools 1.5.2 → 1.6.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.
@@ -13,6 +13,16 @@ import axiosRetry from 'axios-retry';
13
13
  import mocks from '../../test/data/mocks.js'
14
14
  import { logger, LOG_STAGES } from './logger.js';
15
15
 
16
+ const CLOUD_PLATFORM = process.env['IBMCLOUD_PLATFORM_DOMAIN'] || 'cloud.ibm.com';
17
+ const DEV_MODE = CLOUD_PLATFORM !== 'cloud.ibm.com';
18
+ const IAM_BASE_URL = DEV_MODE ? process.env['IBMCLOUD_IAM_API_ENDPOINT'] : 'https://iam.cloud.ibm.com';
19
+ const GHOST_BASE_URL = DEV_MODE ? process.env['IBMCLOUD_GS_API_ENDPOINT'] : 'https://api.global-search-tagging.cloud.ibm.com';
20
+ const DEVOPS_BASE_URL = DEV_MODE ? process.env['IBMCLOUD_DEVOPS_URL'] : 'https://cloud.ibm.com/devops';
21
+ const TOOLCHAIN_BASE_ENDPOINT = DEV_MODE ? process.env['IBMCLOUD_TOOLCHAIN_ENDPOINT'] : '';
22
+ const PIPELINE_BASE_ENDPOINT = DEV_MODE ? process.env['IBMCLOUD_TEKTON_PIPELINE_ENDPOINT'] : '';
23
+ const GIT_BASE_ENDPOINT = DEV_MODE ? process.env['IBMCLOUD_GIT_ENDPOINT'] : '';
24
+ const OTC_BASE_ENDPOINT = DEV_MODE ? process.env['IBMCLOUD_OTC_ENDPOINT'] : '';
25
+
16
26
  const MOCK_ALL_REQUESTS = process.env.MOCK_ALL_REQUESTS === 'true' || 'false';
17
27
 
18
28
  axiosRetry(axios, {
@@ -23,13 +33,15 @@ axiosRetry(axios, {
23
33
  },
24
34
  });
25
35
 
36
+ axios.defaults.timeout = 10000; // 10 seconds
37
+
26
38
  axios.interceptors.request.use(request => {
27
- logger.debug(`${request.method.toUpperCase()} ${request.url}`, LOG_STAGES.setup);
39
+ logger.debug(`${request.method.toUpperCase()} ${request.url}`, LOG_STAGES.request);
28
40
  if (request.data) {
29
41
  const body = typeof request.data === 'string'
30
42
  ? request.data
31
43
  : JSON.stringify(request.data);
32
- logger.log(`Https Request body: ${body}`, LOG_STAGES.setup);
44
+ logger.log(`Https Request body: ${body}`, LOG_STAGES.request);
33
45
  }
34
46
  return request;
35
47
  });
@@ -41,21 +53,21 @@ axios.interceptors.response.use(response => {
41
53
  : JSON.stringify(response.data);
42
54
  if (response.data.access_token) // Redact user access token in logs
43
55
  body = body.replaceAll(response.data.access_token, '<USER ACCESS TOKEN>');
44
- logger.log(`Https Response body: ${body}`, LOG_STAGES.setup);
56
+ logger.log(`Https Response body: ${body}`, LOG_STAGES.request);
45
57
  }
46
58
  return response;
47
59
  }, error => {
48
60
  if (error.response) {
49
- logger.log(`Error response status: ${error.response.status} ${error.response.statusText}`);
50
- logger.log(`Error response body: ${JSON.stringify(error.response.data)}`);
61
+ logger.log(`Error response status: ${error.response.status} ${error.response.statusText}`, LOG_STAGES.request);
62
+ logger.log(`Error response body: ${JSON.stringify(error.response.data)}`, LOG_STAGES.request);
51
63
  } else {
52
- logger.log(`Error message: ${error.message}`);
64
+ logger.log(`Error message: ${error.message}`, LOG_STAGES.request);
53
65
  }
54
66
  return Promise.reject(error);
55
67
  });
56
68
 
57
69
  async function getBearerToken(apiKey) {
58
- const iamUrl = 'https://iam.cloud.ibm.com/identity/token';
70
+ const iamUrl = IAM_BASE_URL + '/identity/token';
59
71
  const params = new URLSearchParams();
60
72
  params.append('grant_type', 'urn:ibm:params:oauth:grant-type:apikey');
61
73
  params.append('apikey', apiKey);
@@ -78,7 +90,7 @@ async function getBearerToken(apiKey) {
78
90
  }
79
91
 
80
92
  async function getAccountId(bearer, apiKey) {
81
- const iamUrl = 'https://iam.cloud.ibm.com/v1/apikeys/details';
93
+ const iamUrl = IAM_BASE_URL + '/v1/apikeys/details';
82
94
  const options = {
83
95
  method: 'GET',
84
96
  url: iamUrl,
@@ -97,7 +109,7 @@ async function getAccountId(bearer, apiKey) {
97
109
  }
98
110
 
99
111
  async function getToolchain(bearer, toolchainId, region) {
100
- const apiBaseUrl = `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
112
+ const apiBaseUrl = TOOLCHAIN_BASE_ENDPOINT || `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
101
113
  const options = {
102
114
  method: 'GET',
103
115
  url: `${apiBaseUrl}/toolchains/${toolchainId}`,
@@ -120,9 +132,8 @@ async function getToolchain(bearer, toolchainId, region) {
120
132
  }
121
133
 
122
134
  async function getToolchainsByName(bearer, accountId, toolchainName) {
123
- const apiBaseUrl = 'https://api.global-search-tagging.cloud.ibm.com/v3';
124
135
  const options = {
125
- url: apiBaseUrl + '/resources/search',
136
+ url: GHOST_BASE_URL + '/v3/resources/search',
126
137
  method: 'POST',
127
138
  headers: {
128
139
  'Authorization': `Bearer ${bearer}`,
@@ -149,9 +160,8 @@ async function getCdInstanceByRegion(bearer, accountId, region) {
149
160
  return mocks.getCdInstanceByRegionResponses[process.env.MOCK_GET_CD_INSTANCE_BY_REGION_SCENARIO].data.items.length > 0;
150
161
  }
151
162
 
152
- const apiBaseUrl = 'https://api.global-search-tagging.cloud.ibm.com/v3';
153
163
  const options = {
154
- url: apiBaseUrl + '/resources/search',
164
+ url: GHOST_BASE_URL + '/v3/resources/search',
155
165
  method: 'POST',
156
166
  headers: {
157
167
  'Authorization': `Bearer ${bearer}`,
@@ -174,7 +184,7 @@ async function getCdInstanceByRegion(bearer, accountId, region) {
174
184
  }
175
185
 
176
186
  async function getToolchainTools(bearer, toolchainId, region) {
177
- const apiBaseUrl = `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
187
+ const apiBaseUrl = TOOLCHAIN_BASE_ENDPOINT || `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
178
188
  const options = {
179
189
  method: 'GET',
180
190
  url: `${apiBaseUrl}/toolchains/${toolchainId}/tools`,
@@ -196,7 +206,7 @@ async function getToolchainTools(bearer, toolchainId, region) {
196
206
  }
197
207
 
198
208
  async function getPipelineData(bearer, pipelineId, region) {
199
- const apiBaseUrl = `https://api.${region}.devops.cloud.ibm.com/pipeline/v2`;
209
+ const apiBaseUrl = PIPELINE_BASE_ENDPOINT || `https://api.${region}.devops.cloud.ibm.com/pipeline/v2`;
200
210
  const options = {
201
211
  method: 'GET',
202
212
  url: `${apiBaseUrl}/tekton_pipelines/${pipelineId}`,
@@ -216,18 +226,17 @@ async function getPipelineData(bearer, pipelineId, region) {
216
226
  }
217
227
  }
218
228
 
219
- // takes in resource group ID or name
220
- async function getResourceGroupIdAndName(bearer, accountId, resourceGroup) {
221
- const apiBaseUrl = 'https://api.global-search-tagging.cloud.ibm.com/v3';
229
+ // takes in list of resource group IDs or names
230
+ async function getResourceGroups(bearer, accountId, resourceGroups) {
222
231
  const options = {
223
- url: apiBaseUrl + '/resources/search',
232
+ url: GHOST_BASE_URL + '/v3/resources/search',
224
233
  method: 'POST',
225
234
  headers: {
226
235
  'Authorization': `Bearer ${bearer}`,
227
236
  'Content-Type': 'application/json',
228
237
  },
229
238
  data: {
230
- 'query': `type:resource-group AND (name:${resourceGroup} OR doc.id:${resourceGroup}) AND doc.state:ACTIVE`,
239
+ 'query': `type:resource-group AND doc.state:ACTIVE AND (${resourceGroups.map(rg => `name:${rg} OR doc.id:${rg}`).join(' OR ')})`,
231
240
  'fields': ['doc.id', 'doc.name']
232
241
  },
233
242
  params: { account_id: accountId },
@@ -236,17 +245,16 @@ async function getResourceGroupIdAndName(bearer, accountId, resourceGroup) {
236
245
  const response = await axios(options);
237
246
  switch (response.status) {
238
247
  case 200:
239
- if (response.data.items.length != 1) throw Error('The resource group with provided ID or name was not found or is not accessible');
240
- return { id: response.data.items[0].doc.id, name: response.data.items[0].doc.name };
248
+ if (response.data.items.length === 0) throw Error('No matching resource groups were found for the provided id(s) or name(s)');
249
+ return response.data.items.map(item => { return { id: item.doc.id, name: item.doc.name } });
241
250
  default:
242
- throw Error('The resource group with provided ID or name was not found or is not accessible');
251
+ throw Error('No matching resource groups were found for the provided id(s) or name(s)');
243
252
  }
244
253
  }
245
254
 
246
255
  async function getAppConfigHealthcheck(bearer, tcId, toolId, region) {
247
- const apiBaseUrl = 'https://cloud.ibm.com/devops/api/v1';
248
256
  const options = {
249
- url: apiBaseUrl + '/appconfig/healthcheck',
257
+ url: DEVOPS_BASE_URL + '/api/v1/appconfig/healthcheck',
250
258
  method: 'GET',
251
259
  headers: {
252
260
  'Authorization': `Bearer ${bearer}`,
@@ -265,9 +273,8 @@ async function getAppConfigHealthcheck(bearer, tcId, toolId, region) {
265
273
  }
266
274
 
267
275
  async function getSecretsHealthcheck(bearer, tcId, toolName, region) {
268
- const apiBaseUrl = 'https://cloud.ibm.com/devops/api/v1';
269
276
  const options = {
270
- url: apiBaseUrl + '/secrets/healthcheck',
277
+ url: DEVOPS_BASE_URL + '/api/v1/secrets/healthcheck',
271
278
  method: 'GET',
272
279
  headers: {
273
280
  'Authorization': `Bearer ${bearer}`,
@@ -286,15 +293,14 @@ async function getSecretsHealthcheck(bearer, tcId, toolName, region) {
286
293
  }
287
294
 
288
295
  async function getGitOAuth(bearer, targetRegion, gitId) {
289
- const url = 'https://cloud.ibm.com/devops/git/api/v1/tokens';
290
296
  const options = {
291
- url: url,
297
+ url: DEVOPS_BASE_URL + '/git/api/v1/tokens',
292
298
  method: 'GET',
293
299
  headers: {
294
300
  'Authorization': `Bearer ${bearer}`,
295
301
  'Content-Type': 'application/json',
296
302
  },
297
- params: { env_id: `ibm:yp:${targetRegion}`, git_id: gitId, console_url: 'https://cloud.ibm.com', return_uri: `https://cloud.ibm.com/devops/git/static/github_return.html` },
303
+ params: { env_id: `ibm:yp:${targetRegion}`, git_id: gitId, console_url: `https://${CLOUD_PLATFORM}`, return_uri: `https://${CLOUD_PLATFORM}/devops/git/static/github_return.html` },
298
304
  validateStatus: () => true
299
305
  };
300
306
  const response = await axios(options);
@@ -309,9 +315,9 @@ async function getGitOAuth(bearer, targetRegion, gitId) {
309
315
  }
310
316
 
311
317
  async function getGritUserProject(privToken, region, user, projectName) {
312
- const url = `https://${region}.git.cloud.ibm.com/api/v4/users/${user}/projects`
318
+ const apiBaseUrl = GIT_BASE_ENDPOINT || `https://${region}.git.cloud.ibm.com/api/v4`;
313
319
  const options = {
314
- url: url,
320
+ url: apiBaseUrl + `/users/${user}/projects`,
315
321
  method: 'GET',
316
322
  headers: {
317
323
  'PRIVATE-TOKEN': privToken
@@ -331,9 +337,9 @@ async function getGritUserProject(privToken, region, user, projectName) {
331
337
  }
332
338
 
333
339
  async function getGritGroup(privToken, region, groupName) {
334
- const url = `https://${region}.git.cloud.ibm.com/api/v4/groups/${groupName}`
340
+ const apiBaseUrl = GIT_BASE_ENDPOINT || `https://${region}.git.cloud.ibm.com/api/v4`;
335
341
  const options = {
336
- url: url,
342
+ url: apiBaseUrl + `/groups/${groupName}`,
337
343
  method: 'GET',
338
344
  headers: {
339
345
  'PRIVATE-TOKEN': privToken
@@ -352,9 +358,9 @@ async function getGritGroup(privToken, region, groupName) {
352
358
  }
353
359
 
354
360
  async function getGritGroupProject(privToken, region, groupId, projectName) {
355
- const url = `https://${region}.git.cloud.ibm.com/api/v4/groups/${groupId}/projects`
361
+ const apiBaseUrl = GIT_BASE_ENDPOINT || `https://${region}.git.cloud.ibm.com/api/v4`;
356
362
  const options = {
357
- url: url,
363
+ url: apiBaseUrl + `/groups/${groupId}/projects`,
358
364
  method: 'GET',
359
365
  headers: {
360
366
  'PRIVATE-TOKEN': privToken
@@ -374,7 +380,7 @@ async function getGritGroupProject(privToken, region, groupId, projectName) {
374
380
  }
375
381
 
376
382
  async function deleteToolchain(bearer, toolchainId, region) {
377
- const apiBaseUrl = `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
383
+ const apiBaseUrl = TOOLCHAIN_BASE_ENDPOINT || `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
378
384
  const options = {
379
385
  method: 'DELETE',
380
386
  url: `${apiBaseUrl}/toolchains/${toolchainId}`,
@@ -394,6 +400,83 @@ async function deleteToolchain(bearer, toolchainId, region) {
394
400
  }
395
401
  }
396
402
 
403
+ async function getSmInstances(bearer, accountId) {
404
+ const options = {
405
+ url: GHOST_BASE_URL + '/v3/resources/search',
406
+ method: 'POST',
407
+ headers: {
408
+ 'Authorization': `Bearer ${bearer}`,
409
+ 'Content-Type': 'application/json',
410
+ },
411
+ data: {
412
+ 'query': `service_name:secrets-manager AND doc.state:ACTIVE`,
413
+ 'fields': ['doc.resource_group_id', 'doc.region_id', 'doc.dashboard_url', 'doc.name', 'doc.guid']
414
+ },
415
+ params: { account_id: accountId },
416
+ validateStatus: () => true
417
+ };
418
+ const response = await axios(options);
419
+ switch (response.status) {
420
+ case 200:
421
+ return response.data.items.map(item => {
422
+ return {
423
+ id: item.doc.guid,
424
+ crn: item.crn,
425
+ name: item.doc.name,
426
+ resource_group_id: item.doc.resource_group_id,
427
+ region_id: item.doc.region_id,
428
+ dashboard_url: item.doc.dashboard_url
429
+ }
430
+ });
431
+ default:
432
+ throw Error('Get Secrets Manager instances failed');
433
+ }
434
+ }
435
+
436
+ async function createTool(bearer, toolchainId, region, params) {
437
+ const apiBaseUrl = TOOLCHAIN_BASE_ENDPOINT || `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
438
+ const options = {
439
+ method: 'POST',
440
+ url: `${apiBaseUrl}/toolchains/${toolchainId}/tools`,
441
+ headers: {
442
+ 'Accept': 'application/json',
443
+ 'Authorization': `Bearer ${bearer}`,
444
+ 'Content-Type': 'application/json',
445
+ },
446
+ data: params,
447
+ validateStatus: () => true
448
+ };
449
+ const response = await axios(options);
450
+ switch (response.status) {
451
+ case 201:
452
+ return response.data;
453
+ default:
454
+ throw Error(response.statusText);
455
+ }
456
+ }
457
+
458
+ async function migrateToolchainSecrets(bearer, data, region) {
459
+ const apiBaseUrl = DEV_MODE ? OTC_BASE_ENDPOINT : `https://otc-api.${region}.devops.cloud.ibm.com/api/v1`;
460
+ const options = {
461
+ method: 'POST',
462
+ url: `${apiBaseUrl}/export_secret`,
463
+ headers: {
464
+ 'Accept': 'application/json',
465
+ 'Authorization': `Bearer ${bearer}`,
466
+ 'Content-Type': 'application/json',
467
+ },
468
+ data: data,
469
+ validateStatus: () => true
470
+ };
471
+ const response = await axios(options);
472
+ switch (response.status) {
473
+ case 201:
474
+ return response.headers.location;
475
+ default:
476
+ throw Error(response.data?.errors.length > 0 ? response.data.errors[0]?.message : response.statusText);
477
+ }
478
+ }
479
+
397
480
  export {
398
481
  getBearerToken,
399
482
  getAccountId,
@@ -402,12 +485,15 @@ export {
402
485
  getToolchainsByName,
403
486
  getToolchainTools,
404
487
  getPipelineData,
405
- getResourceGroupIdAndName,
488
+ getResourceGroups,
406
489
  getAppConfigHealthcheck,
407
490
  getSecretsHealthcheck,
408
491
  getGitOAuth,
409
492
  getGritUserProject,
410
493
  getGritGroup,
411
494
  getGritGroupProject,
412
- deleteToolchain
495
+ deleteToolchain,
496
+ createTool,
497
+ getSmInstances,
498
+ migrateToolchainSecrets
413
499
  }
@@ -18,6 +18,11 @@ import { validateToolchainId, validateGritUrl } from './validate.js';
18
18
  import { logger, LOG_STAGES } from './logger.js';
19
19
  import { getRandChars, promptUserInput, replaceUrlRegion } from './utils.js';
20
20
 
21
+ const CLOUD_PLATFORM = process.env['IBMCLOUD_PLATFORM_DOMAIN'] || 'cloud.ibm.com';
22
+ const DEV_MODE = CLOUD_PLATFORM !== 'cloud.ibm.com';
23
+ const GIT_BASE_URL = DEV_MODE ? process.env['IBMCLOUD_GIT_URL'] : '';
24
+ const IAM_BASE_URL = DEV_MODE ? process.env['IBMCLOUD_IAM_API_ENDPOINT'] : 'https://iam.cloud.ibm.com';
25
+
21
26
  // promisify
22
27
  const readFilePromise = promisify(fs.readFile);
23
28
  const readDirPromise = promisify(fs.readdir);
@@ -36,6 +41,19 @@ async function execPromise(command, options) {
36
41
  function setTerraformEnv(apiKey, verbosity) {
37
42
  if (verbosity >= 2) process.env['TF_LOG'] = 'DEBUG';
38
43
  process.env['TF_VAR_ibmcloud_api_key'] = apiKey;
44
+ // reset all Terraform environment variables if pointing to prod domain
45
+ if (!DEV_MODE) {
46
+ delete process.env['IBMCLOUD_TOOLCHAIN_ENDPOINT'];
47
+ delete process.env['IBMCLOUD_TEKTON_PIPELINE_ENDPOINT'];
48
+ delete process.env['IBMCLOUD_IAM_API_ENDPOINT'];
49
+ delete process.env['IBMCLOUD_USER_MANAGEMENT_ENDPOINT'];
50
+ delete process.env['IBMCLOUD_RESOURCE_MANAGEMENT_API_ENDPOINT'];
51
+ delete process.env['IBMCLOUD_RESOURCE_CONTROLLER_API_ENDPOINT'];
52
+ delete process.env['IBMCLOUD_IS_NG_API_ENDPOINT'];
53
+ delete process.env['IBMCLOUD_GT_API_ENDPOINT'];
54
+ delete process.env['IBMCLOUD_GS_API_ENDPOINT'];
55
+ delete process.env['IBMCLOUD_USER_MANAGEMENT_ENDPOINT'];
56
+ }
39
57
  }
40
58
 
41
59
  async function initProviderFile(targetRegion, dir) {
@@ -177,7 +195,7 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
177
195
  logger.print('Skipping... (URL will remain unchanged in the generatedTerraform configuration)');
178
196
  return '';
179
197
  }
180
- const newUrl = `https://${targetRegion}.git.cloud.ibm.com/${str}.git`;
198
+ const newUrl = (GIT_BASE_URL || `https://${targetRegion}.git.cloud.ibm.com`) + `/${str}.git`;
181
199
  if (usedGritUrls.has(newUrl)) throw Error(`"${newUrl}" has already been used in another mapping entry`);
182
200
  return validateGritUrl(token, targetRegion, str, false);
183
201
  }
@@ -187,10 +205,10 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
187
205
  logger.print('Please enter the new URLs for the following GRIT tool(s) (or submit empty input to skip):\n');
188
206
  }
189
207
 
190
- const newRepoSlug = await promptUserInput(`Old URL: ${thisUrl.slice(0, thisUrl.length - 4)}\nNew URL: https://${targetRegion}.git.cloud.ibm.com/`, '', validateGritUrlPrompt);
208
+ const newRepoSlug = await promptUserInput(`Old URL: ${thisUrl.slice(0, thisUrl.length - 4)}\nNew URL: ${GIT_BASE_URL || 'https://' + targetRegion + '.git.cloud.ibm.com'}`, '', validateGritUrlPrompt);
191
209
 
192
210
  if (newRepoSlug) {
193
- newUrl = `https://${targetRegion}.git.cloud.ibm.com/${newRepoSlug}.git`;
211
+ newUrl = (GIT_BASE_URL || `https://${targetRegion}.git.cloud.ibm.com`) + `/${newRepoSlug}.git`;
194
212
  newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit'][k]['initialization'][0]['repo_url'] = newUrl;
195
213
  attemptAddUsedGritUrl(newUrl);
196
214
  gritMapping[thisUrl] = newUrl;
@@ -473,10 +491,12 @@ function replaceDependsOn(str) {
473
491
 
474
492
  function addS2sScriptToToolchainTf(str) {
475
493
  const provisionerStr = (tfName) => `\n\n provisioner "local-exec" {
476
- command = "node create-s2s-script.js"
494
+ command = "node create-s2s-script.cjs"
477
495
  environment = {
478
496
  IBMCLOUD_API_KEY = var.ibmcloud_api_key
479
497
  TARGET_TOOLCHAIN_ID = ibm_cd_toolchain.${tfName}.id
498
+ IBMCLOUD_PLATFORM = "${CLOUD_PLATFORM}"
499
+ IAM_BASE_URL = "${IAM_BASE_URL}"
480
500
  }\n }`
481
501
  try {
482
502
  if (typeof str === 'string') {
@@ -21,6 +21,32 @@ export function parseEnvVar(name) {
21
21
  return value;
22
22
  };
23
23
 
24
+ export async function promptUserYesNo(question) {
25
+ const rl = readline.createInterface({
26
+ input: process.stdin,
27
+ output: process.stdout
28
+ });
29
+
30
+ const fullPrompt = `${question} [Ctrl-C to abort] (y)es (n)o: `;
31
+
32
+ const answer = await rl.question(fullPrompt);
33
+
34
+ logger.dump(fullPrompt + '\n' + answer + '\n');
35
+ rl.close();
36
+
37
+ const normalized = answer.toLowerCase().trim();
38
+ if (normalized === 'y' || normalized === 'yes') {
39
+ logger.print();
40
+ return true;
41
+ }
42
+ if (normalized === 'n' || normalized === 'no') {
43
+ logger.print();
44
+ return false;
45
+ }
46
+ logger.warn('\nInvalid input. Please enter \'y\' or \'n\'.');
47
+ return await promptUserYesNo(question);
48
+ }
49
+
24
50
  export async function promptUserConfirmation(question, expectedAns, exitMsg) {
25
51
  const rl = readline.createInterface({
26
52
  input: process.stdin,
@@ -33,7 +59,7 @@ export async function promptUserConfirmation(question, expectedAns, exitMsg) {
33
59
  logger.dump(fullPrompt + '\n' + answer + '\n');
34
60
 
35
61
  if (answer.toLowerCase().trim() !== expectedAns) {
36
- logger.print('\n' + exitMsg);
62
+ if (exitMsg) logger.print('\n' + exitMsg);
37
63
  rl.close();
38
64
  await logger.close();
39
65
  process.exit(1);
@@ -59,7 +85,7 @@ export async function promptUserInput(question, initialInput, validationFn) {
59
85
  });
60
86
 
61
87
  rl.prompt(true);
62
- rl.write(initialInput);
88
+ if (initialInput) rl.write(initialInput);
63
89
 
64
90
  for await (const ans of rl) {
65
91
  try {
@@ -72,7 +98,7 @@ export async function promptUserInput(question, initialInput, validationFn) {
72
98
  logger.warn(`Validation failed... ${e.message}`, '', true);
73
99
 
74
100
  rl.prompt(true);
75
- rl.write(initialInput);
101
+ if (initialInput) rl.write(initialInput);
76
102
  }
77
103
  }
78
104
 
@@ -81,6 +107,44 @@ export async function promptUserInput(question, initialInput, validationFn) {
81
107
  return answer.trim();
82
108
  }
83
109
 
110
+ export async function promptUserSelection(question, choices) {
111
+ const rl = readline.createInterface({
112
+ input: process.stdin,
113
+ output: process.stdout
114
+ });
115
+
116
+ rl.on('SIGINT', async () => {
117
+ logger.print('\n' + 'Received SIGINT signal');
118
+ await logger.close();
119
+ process.exit(1);
120
+ });
121
+
122
+ const list = choices
123
+ .map((choice, i) => `[${i + 1}] ${choice}`)
124
+ .join('\n');
125
+
126
+ const promptText = `${question}\n\n${list}\n\nEnter the number of your choice (Ctrl-C to abort): `;
127
+
128
+ let index;
129
+
130
+ while (true) {
131
+ const answer = await rl.question(promptText);
132
+ logger.dump(promptText + '\n' + answer + '\n');
133
+
134
+ index = parseInt(answer.trim(), 10) - 1;
135
+
136
+ if (!Number.isNaN(index) && index >= 0 && index < choices.length) {
137
+ break;
138
+ }
139
+
140
+ logger.warn('\nInvalid choice. Please enter a valid number.\n', '', true);
141
+ }
142
+
143
+ rl.close();
144
+ logger.print();
145
+ return index;
146
+ }
147
+
84
148
  export function replaceUrlRegion(inputUrl, srcRegion, targetRegion) {
85
149
  if (!inputUrl) return '';
86
150
 
@@ -10,9 +10,12 @@
10
10
  import { execSync } from 'child_process';
11
11
  import { logger, LOG_STAGES } from './logger.js'
12
12
  import { RESERVED_GRIT_PROJECT_NAMES, RESERVED_GRIT_GROUP_NAMES, RESERVED_GRIT_SUBGROUP_NAME, TERRAFORM_REQUIRED_VERSION, SECRET_KEYS_MAP } from '../../config.js';
13
- import { getToolchainsByName, getToolchainTools, getPipelineData, getAppConfigHealthcheck, getSecretsHealthcheck, getGitOAuth, getGritUserProject, getGritGroup, getGritGroupProject } from './requests.js';
13
+ import { getToolchainsByName, getToolchainTools, getPipelineData, getAppConfigHealthcheck, getSecretsHealthcheck, getGitOAuth, getGritUserProject, getGritGroupProject } from './requests.js';
14
14
  import { promptUserConfirmation, promptUserInput, isSecretReference } from './utils.js';
15
15
 
16
+ const CLOUD_PLATFORM = process.env['IBMCLOUD_PLATFORM_DOMAIN'] || 'cloud.ibm.com';
17
+ const DEV_MODE = CLOUD_PLATFORM !== 'cloud.ibm.com';
18
+ const GIT_BASE_URL = DEV_MODE ? process.env['IBMCLOUD_GIT_URL'] : '';
16
19
 
17
20
  function validatePrereqsVersions() {
18
21
  const compareVersions = (verInstalled, verRequired) => {
@@ -151,7 +154,7 @@ async function validateTools(token, tcId, region, skipPrompt) {
151
154
  for (const tool of allTools.tools) {
152
155
  const toolName = (tool.name || tool.parameters?.name || tool.parameters?.label || '').replace(/\s+/g, '+');
153
156
  logger.updateSpinnerMsg(`Validating tool \'${toolName}\'`);
154
- const toolUrl = `https://cloud.ibm.com/devops/toolchains/${tool.toolchain_id}/configure/${tool.id}?env_id=ibm:yp:${region}`;
157
+ const toolUrl = `https://${CLOUD_PLATFORM}/devops/toolchains/${tool.toolchain_id}/configure/${tool.id}?env_id=ibm:yp:${region}`;
155
158
 
156
159
  if (tool.state !== 'configured') { // Check for tools in misconfigured/unconfigured/configuring state
157
160
  nonConfiguredTools.push({
@@ -278,7 +281,7 @@ async function validateTools(token, tcId, region, skipPrompt) {
278
281
  }
279
282
 
280
283
  if (toolsWithHashedParams.length > 0) {
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);
284
+ logger.warn('Warning! The following tools contain secrets that cannot be migrated, please use the \'export-secrets\' command to export the secrets: \n', LOG_STAGES.setup, true);
282
285
  logger.table(toolsWithHashedParams);
283
286
  }
284
287
 
@@ -394,7 +397,7 @@ async function validateOAuth(token, tools, targetRegion, skipPrompt) {
394
397
  logger.print(`${o.type}: \x1b[36m${o.link}\x1b[0m\n`);
395
398
  });
396
399
 
397
- if (hasFailedLink) logger.print(`Please manually verify failed authorization(s): https://cloud.ibm.com/devops/git?env_id=ibm:yp:${targetRegion}\n`);
400
+ if (hasFailedLink) logger.print(`Please manually verify failed authorization(s): https://${CLOUD_PLATFORM}/devops/git?env_id=ibm:yp:${targetRegion}\n`);
398
401
 
399
402
  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.');
400
403
  }
@@ -405,8 +408,9 @@ async function validateGritUrl(token, region, url, validateFull) {
405
408
  let trimmed;
406
409
 
407
410
  if (validateFull) {
408
- if (!url.startsWith(`https://${region}.git.cloud.ibm.com/`) || !url.endsWith('.git')) throw Error('Provided full GRIT url is not valid');
409
- trimmed = url.slice(`https://${region}.git.cloud.ibm.com/`.length, url.length - '.git'.length);
411
+ const baseUrl = (GIT_BASE_URL || `https://${region}.git.cloud.ibm.com`) + '/';
412
+ if (!url.startsWith(baseUrl) || !url.endsWith('.git')) throw Error('Provided full GRIT url is not valid');
413
+ trimmed = url.slice(baseUrl.length, url.length - '.git'.length);
410
414
  } else {
411
415
  trimmed = url.trim();
412
416
  }
@@ -16,9 +16,13 @@ if (!API_KEY) throw Error(`Missing 'IBMCLOUD_API_KEY'`);
16
16
  const TC_ID = process.env['TARGET_TOOLCHAIN_ID'];
17
17
  if (!TC_ID) throw Error(`Missing 'TARGET_TOOLCHAIN_ID'`);
18
18
 
19
+ const CLOUD_PLATFORM = process.env['IBMCLOUD_PLATFORM'] || 'cloud.ibm.com';
20
+ if (!CLOUD_PLATFORM) throw Error(`Missing 'IBMCLOUD_PLATFORM'`);
21
+
22
+ const IAM_BASE_URL = process.env['IAM_BASE_URL'] || 'https://iam.cloud.ibm.com';
23
+ if (!IAM_BASE_URL) throw Error(`Missing 'IAM_BASE_URL'`);
24
+
19
25
  const INPUT_PATH = 'create-s2s.json';
20
- const CLOUD_PLATFORM = 'https://cloud.ibm.com';
21
- const IAM_BASE_URL = 'https://iam.cloud.ibm.com';
22
26
 
23
27
  async function getBearer() {
24
28
  const url = `${IAM_BASE_URL}/identity/token`;
@@ -68,8 +72,8 @@ async function getBearer() {
68
72
  }
69
73
  */
70
74
 
71
- async function createS2sAuthPolicy(item) {
72
- const url = `${CLOUD_PLATFORM}/devops/setup/api/v2/s2s_authorization?${new URLSearchParams({
75
+ async function createS2sAuthPolicy(bearer, item) {
76
+ const url = `https://${CLOUD_PLATFORM}/devops/setup/api/v2/s2s_authorization?${new URLSearchParams({
73
77
  toolchainId: TC_ID,
74
78
  serviceId: item['serviceId'],
75
79
  env_id: item['env_id']
@@ -110,10 +114,10 @@ async function createS2sAuthPolicy(item) {
110
114
 
111
115
  // main
112
116
 
113
- const bearer = await getBearer();
117
+ getBearer().then((bearer) => {
118
+ const inputArr = JSON.parse(fs.readFileSync(resolve(INPUT_PATH)));
114
119
 
115
- const inputArr = JSON.parse(fs.readFileSync(resolve(INPUT_PATH)));
116
-
117
- inputArr.forEach(async (item) => {
118
- await createS2sAuthPolicy(item);
120
+ inputArr.forEach(async (item) => {
121
+ await createS2sAuthPolicy(bearer, item);
122
+ });
119
123
  });
package/index.js CHANGED
@@ -10,13 +10,20 @@
10
10
 
11
11
  import { program } from 'commander';
12
12
  import * as commands from './cmd/index.js'
13
+ import { DOCS_URL } from './config.js';
14
+ import { logger } from './cmd/utils/logger.js';
15
+
13
16
  import packageJson from './package.json' with { type: "json" };
14
17
 
18
+ process.on('exit', (code) => {
19
+ if (code !== 0) logger.print(`Need help? Visit ${DOCS_URL} for more troubleshooting information.`);
20
+ });
21
+
15
22
  program
16
- .name(packageJson.name)
17
- .description('Tools and utilities for the IBM Cloud Continuous Delivery service and resources.')
18
- .version(packageJson.version)
19
- .showHelpAfterError();
23
+ .name(packageJson.name)
24
+ .description('Tools and utilities for the IBM Cloud Continuous Delivery service and resources.')
25
+ .version(packageJson.version)
26
+ .showHelpAfterError();
20
27
 
21
28
  for (let i in commands) {
22
29
  program.addCommand(commands[i]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibm-cloud/cd-tools",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "Tools and utilities for the IBM Cloud Continuous Delivery service and resources",
5
5
  "repository": {
6
6
  "type": "git",
package/test/README.md CHANGED
@@ -36,6 +36,6 @@ You can customize the behavior of the tests by defining configuration properties
36
36
  | `TEST_TEMP_DIR` | `string` | `test/.tmp` | The directory to store temporary files generated by test cases |
37
37
  | `TEST_LOG_DIR` | `string` | `test/.test-logs` | The directory to store test run log files |
38
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 |
39
+ | `LOG_DUMP` | `boolean` | `false` | When set to `true`, individual test case's process's log file generation is enabled, and logs are written to `TEST_TEMP_DIR` |
40
40
  | `DISABLE_SPINNER` | `boolean` | `true` | When set to `true`, visual spinner is disabled across all test cases' processes |
41
- | `VERBOSE_MODE` | `boolean` | `false` | When set to `true`, each test case's log output increases |
41
+ | `VERBOSE_MODE` | `boolean` | `false` | When set to `true`, each test case's log output increases |
@@ -3,7 +3,7 @@
3
3
  "TEST_TEMP_DIR": "test/.tmp",
4
4
  "TEST_LOG_DIR": "test/.logs",
5
5
  "IBMCLOUD_API_KEY": "<YOUR IBMCLOUD API KEY>",
6
- "LOG_DUMP": true,
6
+ "LOG_DUMP": false,
7
7
  "DISABLE_SPINNER": true,
8
8
  "VERBOSE_MODE": false
9
9
  }