@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.
- package/cmd/copy-toolchain.js +9 -12
- package/cmd/export-secrets.js +323 -0
- package/cmd/index.js +2 -2
- package/cmd/utils/logger.js +18 -12
- package/cmd/utils/requests.js +125 -39
- package/cmd/utils/terraform.js +24 -4
- package/cmd/utils/utils.js +67 -3
- package/cmd/utils/validate.js +10 -6
- package/create-s2s-script.js +13 -9
- package/index.js +11 -4
- package/package.json +1 -1
- package/test/README.md +2 -2
- package/test/config/local.template.json +1 -1
- package/test/copy-toolchain/functionalities.test.js +0 -2
- package/test/copy-toolchain/input-validation.test.js +2 -4
- package/test/copy-toolchain/tf-import.test.js +0 -3
- package/test/copy-toolchain/tool-validation.test.js +1 -3
- package/cmd/check-secrets.js +0 -111
package/cmd/utils/requests.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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 = '
|
|
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 = '
|
|
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:
|
|
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:
|
|
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
|
|
220
|
-
async function
|
|
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:
|
|
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:${
|
|
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
|
|
240
|
-
return { id:
|
|
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('
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
318
|
+
const apiBaseUrl = GIT_BASE_ENDPOINT || `https://${region}.git.cloud.ibm.com/api/v4`;
|
|
313
319
|
const options = {
|
|
314
|
-
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
|
|
340
|
+
const apiBaseUrl = GIT_BASE_ENDPOINT || `https://${region}.git.cloud.ibm.com/api/v4`;
|
|
335
341
|
const options = {
|
|
336
|
-
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
|
|
361
|
+
const apiBaseUrl = GIT_BASE_ENDPOINT || `https://${region}.git.cloud.ibm.com/api/v4`;
|
|
356
362
|
const options = {
|
|
357
|
-
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
|
-
|
|
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
|
}
|
package/cmd/utils/terraform.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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') {
|
package/cmd/utils/utils.js
CHANGED
|
@@ -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
|
|
package/cmd/utils/validate.js
CHANGED
|
@@ -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,
|
|
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
|
|
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 \'
|
|
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
|
|
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
|
-
|
|
409
|
-
|
|
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
|
}
|
package/create-s2s-script.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
117
|
+
getBearer().then((bearer) => {
|
|
118
|
+
const inputArr = JSON.parse(fs.readFileSync(resolve(INPUT_PATH)));
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
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` |
|
|
41
|
+
| `VERBOSE_MODE` | `boolean` | `false` | When set to `true`, each test case's log output increases |
|