@ibm-cloud/cd-tools 1.1.1 → 1.1.2
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 +5 -10
- package/cmd/utils/requests.js +5 -5
- package/cmd/utils/utils.js +7 -6
- package/cmd/utils/validate.js +19 -51
- package/config.js +2 -1
- package/package.json +1 -1
package/cmd/copy-toolchain.js
CHANGED
|
@@ -16,8 +16,8 @@ import { Command, Option } from 'commander';
|
|
|
16
16
|
import { parseEnvVar } from './utils/utils.js';
|
|
17
17
|
import { logger, LOG_STAGES } from './utils/logger.js';
|
|
18
18
|
import { setTerraformerEnv, setTerraformEnv, initProviderFile, runTerraformerImport, setupTerraformFiles, runTerraformInit, getNumResourcesPlanned, runTerraformApply, getNumResourcesCreated, getNewToolchainId } from './utils/terraform.js';
|
|
19
|
-
import { getAccountId, getBearerToken,
|
|
20
|
-
import { validatePrereqsVersions,
|
|
19
|
+
import { getAccountId, getBearerToken, getResourceGroupIdAndName, getToolchain } from './utils/requests.js';
|
|
20
|
+
import { validatePrereqsVersions, validateTag, validateToolchainId, validateToolchainName, validateTools, validateOAuth, warnDuplicateName, validateGritUrl } from './utils/validate.js';
|
|
21
21
|
|
|
22
22
|
import { COPY_TOOLCHAIN_DESC, MIGRATION_DOC_URL, TARGET_REGIONS, SOURCE_REGIONS } from '../config.js';
|
|
23
23
|
|
|
@@ -84,6 +84,7 @@ async function main(options) {
|
|
|
84
84
|
let targetToolchainName = options.name;
|
|
85
85
|
let targetTag = options.tag;
|
|
86
86
|
let targetRgId;
|
|
87
|
+
let targetRgName;
|
|
87
88
|
let apiKey = options.apikey;
|
|
88
89
|
let moreTfResources = {};
|
|
89
90
|
let gritMapping = {};
|
|
@@ -137,17 +138,11 @@ async function main(options) {
|
|
|
137
138
|
exit(1);
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
// reuse resource group if not provided
|
|
142
|
-
targetRgId = sourceToolchainData['resource_group_id'];
|
|
143
|
-
} else {
|
|
144
|
-
targetRgId = await getResourceGroupId(bearer, accountId, targetRg);
|
|
145
|
-
validateResourceGroupId(targetRgId);
|
|
146
|
-
}
|
|
141
|
+
({ id: targetRgId, name: targetRgName } = await getResourceGroupIdAndName(bearer, accountId, targetRg || sourceToolchainData['resource_group_id']));
|
|
147
142
|
|
|
148
143
|
// reuse name if not provided
|
|
149
144
|
if (!targetToolchainName) targetToolchainName = sourceToolchainData['name'];
|
|
150
|
-
[targetToolchainName, targetTag] = await warnDuplicateName(bearer, accountId, targetToolchainName, sourceRegion, targetRegion, targetRgId, targetTag, skipUserConfirmation);
|
|
145
|
+
[targetToolchainName, targetTag] = await warnDuplicateName(bearer, accountId, targetToolchainName, sourceRegion, targetRegion, targetRgId, targetRgName, targetTag, skipUserConfirmation);
|
|
151
146
|
|
|
152
147
|
const allTools = await logger.withSpinner(validateTools,
|
|
153
148
|
'Validating Toolchain Tool(s)...',
|
package/cmd/utils/requests.js
CHANGED
|
@@ -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
|
|
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
|
}
|
|
@@ -349,11 +349,11 @@ export {
|
|
|
349
349
|
getToolchainsByName,
|
|
350
350
|
getToolchainTools,
|
|
351
351
|
getPipelineData,
|
|
352
|
-
|
|
352
|
+
getResourceGroupIdAndName,
|
|
353
353
|
getAppConfigHealthcheck,
|
|
354
354
|
getSecretsHealthcheck,
|
|
355
355
|
getGitOAuth,
|
|
356
356
|
getGritUserProject,
|
|
357
357
|
getGritGroup,
|
|
358
358
|
getGritGroupProject
|
|
359
|
-
}
|
|
359
|
+
}
|
package/cmd/utils/utils.js
CHANGED
|
@@ -25,7 +25,7 @@ export async function promptUserConfirmation(question, expectedAns, exitMsg) {
|
|
|
25
25
|
output: process.stdout
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
const fullPrompt = question + `\n\nOnly '${expectedAns}' will be accepted to proceed
|
|
28
|
+
const fullPrompt = question + `\n\nOnly '${expectedAns}' will be accepted to proceed. (Ctrl-C to abort)\n\nEnter a value: `;
|
|
29
29
|
const answer = await rl.question(fullPrompt);
|
|
30
30
|
|
|
31
31
|
logger.dump(fullPrompt + '\n' + answer + '\n');
|
|
@@ -38,7 +38,7 @@ export async function promptUserConfirmation(question, expectedAns, exitMsg) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
rl.close();
|
|
41
|
-
logger.print(
|
|
41
|
+
logger.print();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export async function promptUserInput(question, initialInput, validationFn) {
|
|
@@ -75,6 +75,7 @@ export async function promptUserInput(question, initialInput, validationFn) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
rl.close();
|
|
78
|
+
logger.print();
|
|
78
79
|
return answer.trim();
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -97,12 +98,12 @@ export function replaceUrlRegion(inputUrl, srcRegion, targetRegion) {
|
|
|
97
98
|
*
|
|
98
99
|
* @param {String} crn - The crn to decompose.
|
|
99
100
|
**/
|
|
100
|
-
export function decomposeCrn
|
|
101
|
+
export function decomposeCrn(crn) {
|
|
101
102
|
const crnParts = crn.split(':');
|
|
102
103
|
|
|
103
104
|
// Remove the 'a/' segment.
|
|
104
105
|
let accountId = crnParts[6];
|
|
105
|
-
if(accountId) {
|
|
106
|
+
if (accountId) {
|
|
106
107
|
accountId = accountId.split('/')[1];
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -123,6 +124,6 @@ export function decomposeCrn (crn) {
|
|
|
123
124
|
*
|
|
124
125
|
* @param {String} value - The value to verify.
|
|
125
126
|
**/
|
|
126
|
-
export function isSecretReference
|
|
127
|
+
export function isSecretReference(value) {
|
|
127
128
|
return !!(VAULT_REGEX.find(r => r.test(value)));
|
|
128
|
-
};
|
|
129
|
+
};
|
package/cmd/utils/validate.js
CHANGED
|
@@ -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, TERRAFORMER_REQUIRED_VERSION, UPDATEABLE_SECRET_PROPERTIES_BY_TOOL_TYPE } 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);
|
|
@@ -95,18 +77,6 @@ function validateToolchainName(tcName) {
|
|
|
95
77
|
throw Error('Provided toolchain name is invalid');
|
|
96
78
|
}
|
|
97
79
|
|
|
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
80
|
function validateTag(tag) {
|
|
111
81
|
if (typeof tag != 'string') throw Error('Provided resource group ID is not a string');
|
|
112
82
|
const trimmed = tag.trim();
|
|
@@ -120,7 +90,7 @@ function validateTag(tag) {
|
|
|
120
90
|
}
|
|
121
91
|
|
|
122
92
|
|
|
123
|
-
async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegion, targetResourceGroupId, targetTag, skipPrompt) {
|
|
93
|
+
async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegion, targetResourceGroupId, targetResourceGroupName, targetTag, skipPrompt) {
|
|
124
94
|
const toolchains = await getToolchainsByName(token, accountId, tcName);
|
|
125
95
|
|
|
126
96
|
let hasSameRegion = false;
|
|
@@ -145,19 +115,19 @@ async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegi
|
|
|
145
115
|
|
|
146
116
|
if (hasBoth) {
|
|
147
117
|
// warning! prompt user to cancel, rename (e.g. add a suffix) or continue
|
|
148
|
-
logger.warn(
|
|
118
|
+
logger.warn(`\nWarning! A toolchain named '${tcName}' already exists in:\n - Region: ${targetRegion}\n - Resource Group: ${targetResourceGroupName} (${targetResourceGroupId})`, '', true);
|
|
149
119
|
|
|
150
120
|
if (!skipPrompt) {
|
|
151
|
-
newTcName = await promptUserInput(
|
|
121
|
+
newTcName = await promptUserInput(`\n(Recommended) Edit the cloned toolchain's name [default: ${tcName}] (Ctrl-C to abort):\n`, tcName, validateToolchainName);
|
|
152
122
|
}
|
|
153
123
|
} else {
|
|
154
124
|
if (hasSameRegion) {
|
|
155
125
|
// soft warning of confusion
|
|
156
|
-
logger.warn(
|
|
126
|
+
logger.warn(`\nWarning! A toolchain named '${tcName}' already exists in:\n - Region: ${targetRegion}`, '', true);
|
|
157
127
|
}
|
|
158
128
|
if (hasSameResourceGroup) {
|
|
159
129
|
// soft warning of confusion
|
|
160
|
-
logger.warn(
|
|
130
|
+
logger.warn(`\nWarning! A toolchain named '${tcName}' already exists in:\n - Resource Group: ${targetResourceGroupName} (${targetResourceGroupId})`, '', true);
|
|
161
131
|
}
|
|
162
132
|
}
|
|
163
133
|
|
|
@@ -169,7 +139,7 @@ async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegi
|
|
|
169
139
|
if (str.trim() === '') return null;
|
|
170
140
|
return validateTag(str);
|
|
171
141
|
}
|
|
172
|
-
newTag = await promptUserInput('(Recommended) Add a tag to the cloned toolchain:\n', `cloned-from-${srcRegion}`, validateTagOrEmpty);
|
|
142
|
+
newTag = await promptUserInput('\n(Recommended) Add a tag to the cloned toolchain (Ctrl-C to abort):\n', `cloned-from-${srcRegion}`, validateTagOrEmpty);
|
|
173
143
|
}
|
|
174
144
|
}
|
|
175
145
|
return [newTcName, newTag];
|
|
@@ -264,7 +234,7 @@ async function validateTools(token, tcId, region, skipPrompt) {
|
|
|
264
234
|
});
|
|
265
235
|
}
|
|
266
236
|
else {
|
|
267
|
-
const secretsToCheck =
|
|
237
|
+
const secretsToCheck = UPDATEABLE_SECRET_PROPERTIES_BY_TOOL_TYPE[tool.tool_type_id] || []; // Check for secrets in the rest of the tools
|
|
268
238
|
Object.entries(tool.parameters).forEach(([key, value]) => {
|
|
269
239
|
if (secretPattern.test(value) && secretsToCheck.includes(key)) secrets.push(key);
|
|
270
240
|
});
|
|
@@ -279,14 +249,17 @@ async function validateTools(token, tcId, region, skipPrompt) {
|
|
|
279
249
|
}
|
|
280
250
|
}
|
|
281
251
|
}
|
|
282
|
-
const
|
|
252
|
+
const hasInvalidConfig = nonConfiguredTools.length > 0 || patTools.length > 0 || toolsWithHashedParams.length > 0;
|
|
283
253
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
logger.
|
|
287
|
-
logger.
|
|
254
|
+
if (classicPipelines.length > 0) {
|
|
255
|
+
logger.failSpinner('Unsupported tools found!');
|
|
256
|
+
logger.warn('Warning! Classic pipelines are currently not supported in migration:\n', LOG_STAGES.setup, true);
|
|
257
|
+
logger.table(classicPipelines);
|
|
288
258
|
}
|
|
289
259
|
|
|
260
|
+
if (hasInvalidConfig) logger.failSpinner('Configuration problems found!');
|
|
261
|
+
if (classicPipelines.length > 0 || hasInvalidConfig) logger.resetSpinner(); // Manually reset spinner to prevent duplicate spinners
|
|
262
|
+
|
|
290
263
|
if (nonConfiguredTools.length > 0) {
|
|
291
264
|
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
265
|
logger.table(nonConfiguredTools);
|
|
@@ -297,17 +270,13 @@ async function validateTools(token, tcId, region, skipPrompt) {
|
|
|
297
270
|
logger.table(patTools);
|
|
298
271
|
}
|
|
299
272
|
|
|
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
273
|
if (toolsWithHashedParams.length > 0) {
|
|
306
274
|
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
275
|
logger.table(toolsWithHashedParams);
|
|
308
276
|
}
|
|
309
277
|
|
|
310
|
-
if (!skipPrompt &&
|
|
278
|
+
if (!skipPrompt && (classicPipelines.length > 0 || hasInvalidConfig))
|
|
279
|
+
await promptUserConfirmation('Caution: The above tool(s) will not be properly configured post migration. Do you want to proceed?', 'yes', 'Toolchain migration cancelled.');
|
|
311
280
|
|
|
312
281
|
return allTools.tools;
|
|
313
282
|
}
|
|
@@ -414,7 +383,7 @@ async function validateOAuth(token, tools, targetRegion, skipPrompt) {
|
|
|
414
383
|
|
|
415
384
|
logger.print('Authorize using the following links: \n');
|
|
416
385
|
oauthLinks.forEach((o) => {
|
|
417
|
-
logger.print(`${o.type}: \x1b[
|
|
386
|
+
logger.print(`${o.type}: \x1b[36m${o.link}\x1b[0m\n`);
|
|
418
387
|
});
|
|
419
388
|
|
|
420
389
|
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.');
|
|
@@ -494,7 +463,6 @@ export {
|
|
|
494
463
|
validatePrereqsVersions,
|
|
495
464
|
validateToolchainId,
|
|
496
465
|
validateToolchainName,
|
|
497
|
-
validateResourceGroupId,
|
|
498
466
|
validateTag,
|
|
499
467
|
validateTools,
|
|
500
468
|
validateOAuth,
|
package/config.js
CHANGED