@ibm-cloud/cd-tools 1.5.3 → 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 +7 -3
- 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/copy-toolchain.js
CHANGED
|
@@ -22,25 +22,22 @@ import { Command, Option } from 'commander';
|
|
|
22
22
|
import { parseEnvVar } from './utils/utils.js';
|
|
23
23
|
import { logger, LOG_STAGES } from './utils/logger.js';
|
|
24
24
|
import { setTerraformEnv, initProviderFile, setupTerraformFiles, runTerraformInit, getNumResourcesPlanned, runTerraformApply, getNumResourcesCreated, getNewToolchainId } from './utils/terraform.js';
|
|
25
|
-
import { getAccountId, getBearerToken, getCdInstanceByRegion,
|
|
25
|
+
import { getAccountId, getBearerToken, getCdInstanceByRegion, getResourceGroups, getToolchain } from './utils/requests.js';
|
|
26
26
|
import { validatePrereqsVersions, validateTag, validateToolchainId, validateToolchainName, validateTools, validateOAuth, warnDuplicateName, validateGritUrl } from './utils/validate.js';
|
|
27
27
|
import { importTerraform } from './utils/import-terraform.js';
|
|
28
28
|
|
|
29
|
-
import { COPY_TOOLCHAIN_DESC,
|
|
29
|
+
import { COPY_TOOLCHAIN_DESC, TARGET_REGIONS, SOURCE_REGIONS } from '../config.js';
|
|
30
30
|
|
|
31
31
|
import packageJson from '../package.json' with { type: "json" };
|
|
32
32
|
|
|
33
|
-
process.on('exit', (code) => {
|
|
34
|
-
if (code !== 0) logger.print(`Need help? Visit ${DOCS_URL} for more troubleshooting information.`);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
33
|
const TIME_SUFFIX = new Date().getTime();
|
|
38
34
|
const LOGS_DIR = '.logs';
|
|
39
35
|
const TEMP_DIR = '.migration-temp-' + TIME_SUFFIX;
|
|
40
36
|
const LOG_DUMP = process.env['LOG_DUMP'] === 'false' ? false : true; // when true or not specified, logs are also written to a log file in LOGS_DIR
|
|
41
|
-
const DEBUG_MODE = process.env['DEBUG_MODE'] === 'true'
|
|
37
|
+
const DEBUG_MODE = process.env['DEBUG_MODE'] === 'true'; // when true, temp folder is preserved
|
|
42
38
|
const OUTPUT_DIR = 'output-' + TIME_SUFFIX;
|
|
43
39
|
const DRY_RUN = false; // when true, terraform apply does not run
|
|
40
|
+
const CLOUD_PLATFORM = process.env['IBMCLOUD_PLATFORM_DOMAIN'] || 'cloud.ibm.com';
|
|
44
41
|
|
|
45
42
|
|
|
46
43
|
const command = new Command('copy-toolchain')
|
|
@@ -106,7 +103,7 @@ async function main(options) {
|
|
|
106
103
|
// Validate arguments are valid and check if Terraform is installed appropriately
|
|
107
104
|
try {
|
|
108
105
|
validatePrereqsVersions();
|
|
109
|
-
|
|
106
|
+
logger.info(`\x1b[32m✔\x1b[0m cd-tools Version: ${packageJson.version}`, LOG_STAGES.setup);
|
|
110
107
|
|
|
111
108
|
if (!apiKey) apiKey = parseEnvVar('IBMCLOUD_API_KEY');
|
|
112
109
|
bearer = await getBearerToken(apiKey);
|
|
@@ -163,8 +160,8 @@ async function main(options) {
|
|
|
163
160
|
exit(1);
|
|
164
161
|
}
|
|
165
162
|
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
const resourceGroups = await getResourceGroups(bearer, accountId, [targetRg || sourceToolchainData['resource_group_id']]);
|
|
164
|
+
({ id: targetRgId, name: targetRgName } = resourceGroups[0])
|
|
168
165
|
// reuse name if not provided
|
|
169
166
|
if (!targetToolchainName) targetToolchainName = sourceToolchainData['name'];
|
|
170
167
|
[targetToolchainName, targetTag] = await warnDuplicateName(bearer, accountId, targetToolchainName, sourceRegion, targetRegion, targetRgId, targetRgName, targetTag, skipUserConfirmation);
|
|
@@ -321,7 +318,7 @@ async function main(options) {
|
|
|
321
318
|
|
|
322
319
|
// copy script
|
|
323
320
|
const s2sScript = fs.readFileSync(resolve(__dirname, '../create-s2s-script.js'));
|
|
324
|
-
fs.writeFileSync(resolve(`${outputDir}/create-s2s-script.
|
|
321
|
+
fs.writeFileSync(resolve(`${outputDir}/create-s2s-script.cjs`), s2sScript);
|
|
325
322
|
}
|
|
326
323
|
|
|
327
324
|
// create toolchain, which invokes script to create s2s if applicable
|
|
@@ -341,7 +338,7 @@ async function main(options) {
|
|
|
341
338
|
|
|
342
339
|
logger.print('\n');
|
|
343
340
|
logger.info(`Toolchain "${sourceToolchainData['name']}" from ${sourceRegion} was cloned to "${targetToolchainName ?? sourceToolchainData['name']}" in ${targetRegion} ${applyErrors ? 'with some errors' : 'successfully'}, with ${numResourcesCreated} / ${numResourcesPlanned} resources created!`, LOG_STAGES.info);
|
|
344
|
-
if (newTcId) logger.info(`See cloned toolchain: https
|
|
341
|
+
if (newTcId) logger.info(`See cloned toolchain: https://${CLOUD_PLATFORM}/devops/toolchains/${newTcId}?env_id=ibm:yp:${targetRegion}`, LOG_STAGES.info, true);
|
|
345
342
|
} else {
|
|
346
343
|
logger.info(`DRY_RUN: ${dryRun}, skipping terraform apply...`, LOG_STAGES.tf);
|
|
347
344
|
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed Materials - Property of IBM
|
|
3
|
+
* (c) Copyright IBM Corporation 2025. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* Note to U.S. Government Users Restricted Rights:
|
|
6
|
+
* Use, duplication or disclosure restricted by GSA ADP Schedule
|
|
7
|
+
* Contract with IBM Corp.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
import { exit } from 'node:process';
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { parseEnvVar, decomposeCrn, isSecretReference, promptUserSelection, promptUserYesNo, promptUserInput } from './utils/utils.js';
|
|
14
|
+
import { logger, LOG_STAGES } from './utils/logger.js';
|
|
15
|
+
import { getBearerToken, getToolchain, getToolchainTools, getPipelineData, getSmInstances, createTool, getAccountId, getResourceGroups, migrateToolchainSecrets } from './utils/requests.js';
|
|
16
|
+
import { SECRET_KEYS_MAP } from '../config.js';
|
|
17
|
+
|
|
18
|
+
const CLOUD_PLATFORM = process.env['IBMCLOUD_PLATFORM_DOMAIN'] || 'cloud.ibm.com';
|
|
19
|
+
|
|
20
|
+
const command = new Command('export-secrets')
|
|
21
|
+
.description('Exports Toolchain stored secrets to a Secrets Manager instance')
|
|
22
|
+
.requiredOption('-c, --toolchain-crn <crn>', 'The CRN of the toolchain to check')
|
|
23
|
+
.option('-a, --apikey <api_key>', 'API key used to authenticate. Must have IAM permission to read toolchains and create secrets in Secrets Manager')
|
|
24
|
+
.option('--check', '(Optional) Checks and lists any stored secrets in your toolchain')
|
|
25
|
+
.option('-v, --verbose', '(Optional) Increase log output')
|
|
26
|
+
.showHelpAfterError()
|
|
27
|
+
.hook('preAction', cmd => cmd.showHelpAfterError(false)) // only show help during validation
|
|
28
|
+
.action(main);
|
|
29
|
+
|
|
30
|
+
async function main(options) {
|
|
31
|
+
const toolchainCrn = options.toolchainCrn;
|
|
32
|
+
const verbosity = options.verbose ? 2 : 1;
|
|
33
|
+
const runMigration = !options.check;
|
|
34
|
+
|
|
35
|
+
logger.setVerbosity(verbosity);
|
|
36
|
+
|
|
37
|
+
let apiKey;
|
|
38
|
+
let bearer;
|
|
39
|
+
let accountId;
|
|
40
|
+
let toolchainId;
|
|
41
|
+
let region;
|
|
42
|
+
let toolchainData;
|
|
43
|
+
let tools;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const toolResults = [];
|
|
47
|
+
const pipelineResults = [];
|
|
48
|
+
|
|
49
|
+
apiKey = options.apikey || parseEnvVar('IBMCLOUD_API_KEY');
|
|
50
|
+
if (!apiKey) {
|
|
51
|
+
logger.error('Missing IBM Cloud IAM API key', LOG_STAGES.setup);
|
|
52
|
+
exit(1);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const decomposedCrn = decomposeCrn(toolchainCrn);
|
|
57
|
+
toolchainId = decomposedCrn.serviceInstance;
|
|
58
|
+
region = decomposedCrn.location;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
throw Error('Provided toolchain CRN is invalid');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Display toolchain data to user
|
|
64
|
+
const getToolchainData = async () => {
|
|
65
|
+
bearer = await getBearerToken(apiKey);
|
|
66
|
+
toolchainData = await getToolchain(bearer, toolchainId, region);
|
|
67
|
+
}
|
|
68
|
+
await logger.withSpinner(getToolchainData, `Reading Toolchain`, 'Valid Toolchain found!');
|
|
69
|
+
logger.print(`Name: ${toolchainData.name}\nRegion: ${region}\nResource Group ID: ${toolchainData.resource_group_id}\nURL:https://${CLOUD_PLATFORM}/devops/toolchains/${toolchainId}?env_id=ibm:yp:${region}\n`);
|
|
70
|
+
|
|
71
|
+
// Check for plain-text secrets in all tools
|
|
72
|
+
const exportSecrets = async () => {
|
|
73
|
+
const getToolsRes = await getToolchainTools(bearer, toolchainId, region);
|
|
74
|
+
tools = getToolsRes.tools;
|
|
75
|
+
|
|
76
|
+
if (tools.length > 0) {
|
|
77
|
+
for (let i = 0; i < tools.length; i++) {
|
|
78
|
+
const tool = tools[i];
|
|
79
|
+
const toolUrl = `https://${CLOUD_PLATFORM}/devops/toolchains/${tool.toolchain_id}/configure/${tool.id}?env_id=ibm:yp:${region}`;
|
|
80
|
+
const toolName = (tool.name || tool.parameters?.name || tool.parameters?.label || '').replace(/\s+/g, '+');
|
|
81
|
+
|
|
82
|
+
// Skip iff it's GitHub/GitLab/GRIT integration with OAuth
|
|
83
|
+
if (['githubconsolidated', 'github_integrated', 'gitlab', 'hostedgit'].includes(tool.tool_type_id) && (tool.parameters?.auth_type === '' || tool.parameters?.auth_type === 'oauth'))
|
|
84
|
+
continue;
|
|
85
|
+
|
|
86
|
+
// Check tool integrations for any plain text secret values
|
|
87
|
+
if (SECRET_KEYS_MAP[tool.tool_type_id]) {
|
|
88
|
+
SECRET_KEYS_MAP[tool.tool_type_id].forEach((entry) => {
|
|
89
|
+
const updateableSecretParam = entry.key;
|
|
90
|
+
if (tool.parameters[updateableSecretParam] && !isSecretReference(tool.parameters[updateableSecretParam]) && tool.parameters[updateableSecretParam].length > 0) {
|
|
91
|
+
toolResults.push({
|
|
92
|
+
'Tool ID': tool.id,
|
|
93
|
+
'Tool Name': toolName,
|
|
94
|
+
'Tool Type': tool.tool_type_id,
|
|
95
|
+
'Property Name': updateableSecretParam,
|
|
96
|
+
'Url': toolUrl
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// For tekton pipelines, check for any plain text secret properties
|
|
103
|
+
if (tool.tool_type_id === 'pipeline' && tool.parameters?.type === 'tekton') {
|
|
104
|
+
const pipelineBaseUrl = `https://${CLOUD_PLATFORM}/devops/pipelines/tekton/${tool.id}`
|
|
105
|
+
const pipelineData = await getPipelineData(bearer, tool.id, region);
|
|
106
|
+
|
|
107
|
+
pipelineData?.properties.forEach((prop) => {
|
|
108
|
+
if (prop.type === 'secure' && !isSecretReference(prop.value) && prop.value.length > 0) {
|
|
109
|
+
pipelineResults.push({
|
|
110
|
+
'Pipeline ID': pipelineData.id,
|
|
111
|
+
'Pipeline Name': toolName,
|
|
112
|
+
'Trigger Name': '',
|
|
113
|
+
'Property Name': prop.name,
|
|
114
|
+
'Url': pipelineBaseUrl + `/config/envProperties?env_id=ibm:yp:${region}`,
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pipelineData?.triggers.forEach((trigger) => {
|
|
120
|
+
trigger.properties?.forEach((prop) => {
|
|
121
|
+
if (prop.type === 'secure' && !isSecretReference(prop.value) && prop.value.length > 0) {
|
|
122
|
+
pipelineResults.push({
|
|
123
|
+
'Pipeline ID': pipelineData.id,
|
|
124
|
+
'Pipeline Name': toolName,
|
|
125
|
+
'Trigger Name': trigger.name,
|
|
126
|
+
'Trigger ID': trigger.id,
|
|
127
|
+
'Property Name': prop.name,
|
|
128
|
+
'Url': pipelineBaseUrl + `?env_id=ibm:yp:${region}`
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
await logger.withSpinner(exportSecrets, `Checking secrets for toolchain ${toolchainCrn}`, 'Secret check complete!');
|
|
139
|
+
|
|
140
|
+
const numTotalSecrets = toolResults.length + pipelineResults.length;
|
|
141
|
+
if (numTotalSecrets > 0) {
|
|
142
|
+
logger.warn(`\nNote: ${numTotalSecrets} locally stored secret(s) found!`)
|
|
143
|
+
} else {
|
|
144
|
+
logger.success('\nNo locally stored secrets found!');
|
|
145
|
+
}
|
|
146
|
+
if (toolResults.length > 0) {
|
|
147
|
+
logger.print();
|
|
148
|
+
logger.print('The following plain text properties were found in tool integrations bound to the toolchain:');
|
|
149
|
+
logger.table(toolResults, 'Url');
|
|
150
|
+
}
|
|
151
|
+
if (pipelineResults.length > 0) {
|
|
152
|
+
logger.print();
|
|
153
|
+
logger.print('The following plain text properties were found in Tekton pipeline(s) bound to the toolchain:');
|
|
154
|
+
logger.table(pipelineResults, 'Url', ['Trigger ID']);
|
|
155
|
+
}
|
|
156
|
+
if (numTotalSecrets > 0 && !runMigration) {
|
|
157
|
+
logger.warn(`\nNote: ${numTotalSecrets} locally stored secret(s) found!\nSecrets stored locally in Toolchains and Pipelines will not be exported when copying a toolchain. It is recommended that secrets be moved to a Secrets Manager instance and converted to secret references if these secrets are required.`);
|
|
158
|
+
logger.warn(`\nTo migrate secrets to Secrets Manager, ensure that you have provisioned an instance of Secrets Manager which you have write access to and rerun the command without the additional param '--check' to move the secrets into Secrets Manager.`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Facilitate Secrets Migration
|
|
162
|
+
const migrateSecrets = async () => {
|
|
163
|
+
accountId = await getAccountId(bearer, apiKey);
|
|
164
|
+
|
|
165
|
+
let allSmInstances = await getSmInstances(bearer, accountId);
|
|
166
|
+
if (allSmInstances.length === 0) {
|
|
167
|
+
logger.warn('No Secrets Manager instances found. Please create a Secrets Manager instance and try again.');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const resourceGroups = await getResourceGroups(bearer, accountId, allSmInstances.map(inst => inst.resource_group_id));
|
|
172
|
+
const groupNameById = Object.fromEntries(
|
|
173
|
+
resourceGroups.map(g => [g.id, g.name])
|
|
174
|
+
);
|
|
175
|
+
allSmInstances = allSmInstances.map(inst => ({
|
|
176
|
+
...inst,
|
|
177
|
+
resource_group_name: groupNameById[inst.resource_group_id] || 'Unknown'
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const instanceChoice = await promptUserSelection(
|
|
181
|
+
'Select a Secrets Manager Instance to migrate secret(s) to:',
|
|
182
|
+
allSmInstances.map(inst => (`\n Name: ${inst.name} (${inst.id})\n Region: ${inst.region_id}\n Resource Group: ${inst.resource_group_name}`))
|
|
183
|
+
);
|
|
184
|
+
const smInstance = allSmInstances[instanceChoice];
|
|
185
|
+
|
|
186
|
+
// Check if there's an existing Secrets Manager tool integration
|
|
187
|
+
let hasSmIntegration = false;
|
|
188
|
+
for (const tool of tools) {
|
|
189
|
+
if (tool.state === 'configured' && tool.tool_type_id === 'secretsmanager') {
|
|
190
|
+
if (
|
|
191
|
+
(tool.parameters?.['instance-id-type'] === 'instance-name' && tool.parameters?.['instance-name'] === smInstance.name &&
|
|
192
|
+
tool.parameters?.region === smInstance.region_id && tool.parameters?.['resource-group'] === smInstance.resource_group_name) ||
|
|
193
|
+
(tool.parameters?.['instance-id-type'] === 'instance-crn' && tool.parameters?.['instance-crn'] === smInstance.crn)
|
|
194
|
+
) {
|
|
195
|
+
hasSmIntegration = true;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Prompt user to create a Secrets Manager tool integration if it doesn't already exist
|
|
202
|
+
if (!hasSmIntegration) {
|
|
203
|
+
logger.warn('No valid Secrets Manager tool integration found.');
|
|
204
|
+
const toCreateSmTool = await promptUserYesNo(`Create a Secrets Manager tool integration?`);
|
|
205
|
+
if (!toCreateSmTool) {
|
|
206
|
+
logger.warn('Toolchain secrets will not be migrated to Secrets Manager. Please create a Secrets Manager tool integration and try again.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const smToolName = await promptUserInput(`Enter the name of the Secrets Manager tool integration to create [Press 'enter' to skip]: `, '', async (input) => {
|
|
210
|
+
if (input.length > 128) {
|
|
211
|
+
throw new Error('The tool integration name must be between 0 and 128 characters long.');
|
|
212
|
+
}
|
|
213
|
+
// from https://cloud.ibm.com/apidocs/toolchain#create-tool
|
|
214
|
+
else if (input !== '' && !/^([^\x00-\x7F]|[a-zA-Z0-9-._ ])+$/.test(input)) {
|
|
215
|
+
throw new Error('Provided tool integration name contains invalid characters.');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const smToolParams = {
|
|
220
|
+
'tool_type_id': 'secretsmanager',
|
|
221
|
+
'parameters': {
|
|
222
|
+
'name': smToolName || 'Secrets Manager',
|
|
223
|
+
'instance-id-type': 'instance-crn',
|
|
224
|
+
'instance-crn': smInstance.crn,
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
try {
|
|
228
|
+
const smTool = await createTool(bearer, toolchainId, region, smToolParams);
|
|
229
|
+
logger.success(`Secrets Manager tool integration created: ${smTool.parameters.name} (${smTool.id})`);
|
|
230
|
+
const smToolUrl = `https://${CLOUD_PLATFORM}/devops/toolchains/${toolchainId}/configure/${smTool.id}?env_id=ibm:yp:${region}`;
|
|
231
|
+
logger.warn(`Create necessary IAM service authorization for toolchain to access Secrets Manager service instance:\n${smToolUrl}`);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
logger.error(`Failed to create Secrets Manager tool integration: ${e.message}`);
|
|
234
|
+
throw e;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let numSecretsMigrated = 0;
|
|
239
|
+
const allSecrets = toolResults.concat(pipelineResults);
|
|
240
|
+
for (let i = 0; i < allSecrets.length; i++) {
|
|
241
|
+
logger.print('-------');
|
|
242
|
+
const secret = allSecrets[i];
|
|
243
|
+
const toolName = secret['Tool Name'] || secret['Pipeline Name'];
|
|
244
|
+
const toolType = secret['Tool Type'] || 'pipeline';
|
|
245
|
+
const toolId = secret['Tool ID'] || secret['Pipeline ID'];
|
|
246
|
+
const triggerId = secret['Trigger ID'];
|
|
247
|
+
const triggerName = secret['Trigger Name'];
|
|
248
|
+
const toolSecretKey = secret['Property Name'];
|
|
249
|
+
const toolSecretUrl = secret['Url'];
|
|
250
|
+
const secretPath = `${toolName || toolType}.${triggerName ? triggerName + '.' : ''}${toolSecretKey}`;
|
|
251
|
+
|
|
252
|
+
logger.print(`[${i + 1}]\n Tool integration: ${toolName ? `'${toolName}' (${toolType})` : toolType}\n Property: '${triggerName ? triggerName + '.' : ''}${toolSecretKey}'\n URL: ${toolSecretUrl}\n`);
|
|
253
|
+
|
|
254
|
+
const shouldMigrateSecret = await promptUserYesNo(`Migrate this secret to Secrets Manager instance '${smInstance.name}'?`);
|
|
255
|
+
if (!shouldMigrateSecret) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const smSecretName = await promptUserInput(`Enter the name of the secret to create [${secretPath}]: `, '', async (input) => {
|
|
260
|
+
if (input.length < 2 || input.length > 256) {
|
|
261
|
+
throw new Error('The secret name must be between 2 and 256 characters long.');
|
|
262
|
+
}
|
|
263
|
+
// from https://cloud.ibm.com/apidocs/secrets-manager/secrets-manager-v2#create-secret
|
|
264
|
+
else if (!/^[A-Za-z0-9_][A-Za-z0-9_]*(?:_*-*\.*[A-Za-z0-9]*)*[A-Za-z0-9]+$/.test(input)) {
|
|
265
|
+
throw new Error('Provided secret name contains invalid characters.');
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const smSecretGroupId = await promptUserInput(`Enter the ID of the secret group to create secret '${smSecretName}' in: `, 'default', async (input) => {
|
|
270
|
+
if (input.length < 7 || input.length > 36) {
|
|
271
|
+
throw new Error('The secret group name must be between 7 and 36 characters long.');
|
|
272
|
+
}
|
|
273
|
+
// from https://cloud.ibm.com/apidocs/secrets-manager/secrets-manager-v2#create-secret
|
|
274
|
+
else if (!/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|default)$/.test(input)) {
|
|
275
|
+
throw new Error('Provided secret group name is invalid. It should be a UUID or the word \'default\'');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const commonProps = {
|
|
281
|
+
toolchain_id: toolchainId,
|
|
282
|
+
destination: {
|
|
283
|
+
is_private: false, // TODO: set this back to 'true' once 'otc-api' has the 'export_secret' endpoint, should always use SM private endpoint
|
|
284
|
+
is_production: CLOUD_PLATFORM === 'cloud.ibm.com',
|
|
285
|
+
secrets_manager_crn: smInstance.crn,
|
|
286
|
+
secret_name: smSecretName,
|
|
287
|
+
secret_group_id: smSecretGroupId
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const payload = {
|
|
291
|
+
source: {
|
|
292
|
+
type: toolType === 'pipeline' ? toolType : 'tool',
|
|
293
|
+
id: toolType === 'pipeline' ? (triggerId || toolId) : toolId,
|
|
294
|
+
secret_key: toolSecretKey,
|
|
295
|
+
kind: toolType === 'pipeline' ? (triggerId ? 'trigger' : 'env') : undefined,
|
|
296
|
+
parent_id: toolType === 'pipeline' ? (triggerId ? toolId : undefined) : undefined
|
|
297
|
+
},
|
|
298
|
+
...commonProps
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const smSecretUrl = await migrateToolchainSecrets(bearer, payload, region);
|
|
302
|
+
logger.success(`Secret successfully migrated!\nSecret URL: ${smSecretUrl}`);
|
|
303
|
+
numSecretsMigrated += 1;
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
logger.error(`Failed to migrate secret '${secretPath}'. Error message: ${e.message}`, '', true);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
logger.success(`Toolchain secrets migration complete, ${numSecretsMigrated} secret(s) successfully migrated.`);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (numTotalSecrets > 0 && runMigration) await migrateSecrets();
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
if (err.message && err.stack) {
|
|
316
|
+
const errMsg = verbosity > 1 ? err.stack : err.message;
|
|
317
|
+
logger.error(errMsg);
|
|
318
|
+
}
|
|
319
|
+
exit(1);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export default command;
|
package/cmd/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Contract with IBM Corp.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import exportSecrets from './export-secrets.js';
|
|
11
11
|
import copyToolchain from './copy-toolchain.js';
|
|
12
12
|
import directTransfer from './direct-transfer.js';
|
|
13
|
-
export {
|
|
13
|
+
export { exportSecrets, copyToolchain, directTransfer };
|
package/cmd/utils/logger.js
CHANGED
|
@@ -60,9 +60,9 @@ class Logger {
|
|
|
60
60
|
|
|
61
61
|
#baseLog(type, msg, prefix) {
|
|
62
62
|
const level = LEVELS[type] || LEVELS.log;
|
|
63
|
-
const formatted = this.#getFullPrefix(prefix) + ' ' + `${level.color}${msg}${COLORS.reset}`;
|
|
63
|
+
const formatted = (prefix ? this.#getFullPrefix(prefix) + ' ' : '') + `${level.color}${msg}${COLORS.reset}`;
|
|
64
64
|
console[level.method](formatted);
|
|
65
|
-
this.logStream?.write(stripAnsi(this.#getFullPrefix(prefix) + `
|
|
65
|
+
this.logStream?.write(stripAnsi((prefix ? this.#getFullPrefix(prefix) + ' ' : '') + `[${type.toUpperCase()}] ` + msg) + '\n');
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
info(msg, prefix = '', force = false) { if (this.verbosity >= 1 || force) this.#baseLog('info', msg, prefix); }
|
|
@@ -132,10 +132,10 @@ class Logger {
|
|
|
132
132
|
return res;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
table(data, rowSpanField = 'url') {
|
|
135
|
+
table(data, rowSpanField = 'url', colsToSkip = []) {
|
|
136
136
|
if (!Array.isArray(data) || data.length < 1) return;
|
|
137
137
|
const tableData = structuredClone(data);
|
|
138
|
-
const headers = Object.keys(tableData[0]).filter(key => key !== rowSpanField);
|
|
138
|
+
const headers = Object.keys(tableData[0]).filter(key => key !== rowSpanField && !colsToSkip.includes(key));
|
|
139
139
|
const t = new Table({
|
|
140
140
|
head: headers,
|
|
141
141
|
style: { head: ['cyan'] }
|
|
@@ -151,13 +151,18 @@ class Logger {
|
|
|
151
151
|
delete row[rowKey];
|
|
152
152
|
}
|
|
153
153
|
tableRow.push(
|
|
154
|
-
...Object.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
...Object.entries(row)
|
|
155
|
+
.filter(([key]) => !colsToSkip.includes(key))
|
|
156
|
+
.map(([_, val]) => {
|
|
157
|
+
if (Array.isArray(val))
|
|
158
|
+
return val
|
|
159
|
+
.map((item, idx) => `${idx + 1}: ${item ?? '-'}`).join('\n');
|
|
160
|
+
else if (val === '')
|
|
161
|
+
return '-';
|
|
162
|
+
else if (typeof val === 'string')
|
|
163
|
+
return val;
|
|
164
|
+
return JSON.stringify(val);
|
|
165
|
+
})
|
|
161
166
|
);
|
|
162
167
|
t.push(tableRow);
|
|
163
168
|
if (rowSpanFieldVal !== '') t.push([{ content: rowSpanFieldVal, colSpan: headers.length - 1 }]);
|
|
@@ -172,5 +177,6 @@ export const LOG_STAGES = {
|
|
|
172
177
|
setup: 'setup',
|
|
173
178
|
import: 'import',
|
|
174
179
|
tf: 'terraform',
|
|
175
|
-
info: 'info'
|
|
180
|
+
info: 'info',
|
|
181
|
+
request: 'request'
|
|
176
182
|
};
|
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`;
|
|
@@ -69,7 +73,7 @@ async function getBearer() {
|
|
|
69
73
|
*/
|
|
70
74
|
|
|
71
75
|
async function createS2sAuthPolicy(bearer, item) {
|
|
72
|
-
const url =
|
|
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']
|
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 |
|
|
@@ -11,8 +11,6 @@ import path from 'node:path';
|
|
|
11
11
|
import nconf from 'nconf';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
|
|
14
|
-
import * as chai from 'chai';
|
|
15
|
-
chai.config.truncateThreshold = 0;
|
|
16
14
|
import { expect, assert } from 'chai';
|
|
17
15
|
|
|
18
16
|
import { assertPtyOutput, assertExecError, areFilesInDir, deleteCreatedToolchains, parseTcIdAndRegion } from '../utils/testUtils.js';
|
|
@@ -11,9 +11,7 @@ import path from 'node:path';
|
|
|
11
11
|
import nconf from 'nconf';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
|
|
14
|
-
import * as chai from 'chai';
|
|
15
14
|
import { expect } from 'chai';
|
|
16
|
-
chai.config.truncateThreshold = 0;
|
|
17
15
|
|
|
18
16
|
import mocks from '../data/mocks.js';
|
|
19
17
|
import { assertExecError, assertPtyOutput } from '../utils/testUtils.js';
|
|
@@ -81,12 +79,12 @@ describe('copy-toolchain: Test user input handling', function () {
|
|
|
81
79
|
{
|
|
82
80
|
name: 'Invalid Resource Group name is provided',
|
|
83
81
|
cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-g', mocks.invalidRgName],
|
|
84
|
-
expected: /
|
|
82
|
+
expected: /No matching resource groups were found for the provided id\(s\) or name\(s\)/,
|
|
85
83
|
},
|
|
86
84
|
{
|
|
87
85
|
name: 'Invalid Resource Group ID is provided',
|
|
88
86
|
cmd: [CLI_PATH, COMMAND, '-c', validCrn, '-r', TARGET_REGIONS[0], '-g', mocks.invalidRgId],
|
|
89
|
-
expected: /
|
|
87
|
+
expected: /No matching resource groups were found for the provided id\(s\) or name\(s\)/,
|
|
90
88
|
},
|
|
91
89
|
{
|
|
92
90
|
name: 'Non-existent GRIT mapping file provided',
|
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import nconf from 'nconf';
|
|
12
12
|
|
|
13
|
-
import * as chai from 'chai';
|
|
14
|
-
chai.config.truncateThreshold = 0;
|
|
15
|
-
|
|
16
13
|
import { assertTfResourcesInDir, assertPtyOutput } from '../utils/testUtils.js';
|
|
17
14
|
import { TEST_TOOLCHAINS } from '../data/test-toolchains.js';
|
|
18
15
|
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import nconf from 'nconf';
|
|
12
12
|
|
|
13
|
-
import * as chai from 'chai';
|
|
14
|
-
chai.config.truncateThreshold = 0;
|
|
15
13
|
import { expect } from 'chai';
|
|
16
14
|
|
|
17
15
|
import { assertPtyOutput, deleteCreatedToolchains } from '../utils/testUtils.js';
|
|
@@ -87,7 +85,7 @@ describe('copy-toolchain: Test tool validation', function () {
|
|
|
87
85
|
timeout: 30000
|
|
88
86
|
},
|
|
89
87
|
assertionFunc: (output) => {
|
|
90
|
-
expect(output).to.match(/Warning! The following GRIT integration\(s\)
|
|
88
|
+
expect(output).to.match(/Warning! The following GRIT integration\(s\) with auth_type "pat" are unsupported during migration and will automatically be converted to auth_type "oauth"/);
|
|
91
89
|
expect(output).to.match(/hostedgit/);
|
|
92
90
|
expect(output).to.match(/Warning! The following tools contain secrets that cannot be migrated/);
|
|
93
91
|
expect(output).to.match(/githubconsolidated[\s\S]*?api_token/);
|
package/cmd/check-secrets.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Licensed Materials - Property of IBM
|
|
3
|
-
* (c) Copyright IBM Corporation 2025. All Rights Reserved.
|
|
4
|
-
*
|
|
5
|
-
* Note to U.S. Government Users Restricted Rights:
|
|
6
|
-
* Use, duplication or disclosure restricted by GSA ADP Schedule
|
|
7
|
-
* Contract with IBM Corp.
|
|
8
|
-
*/
|
|
9
|
-
'use strict';
|
|
10
|
-
|
|
11
|
-
import { exit } from 'node:process';
|
|
12
|
-
import { Command } from 'commander';
|
|
13
|
-
import { parseEnvVar, decomposeCrn, isSecretReference } from './utils/utils.js';
|
|
14
|
-
import { logger, LOG_STAGES } from './utils/logger.js';
|
|
15
|
-
import { getBearerToken, getToolchainTools, getPipelineData } from './utils/requests.js';
|
|
16
|
-
import { SECRET_KEYS_MAP } from '../config.js';
|
|
17
|
-
|
|
18
|
-
const command = new Command('check-secrets')
|
|
19
|
-
.description('Checks if you have any stored secrets in your toolchain or pipelines')
|
|
20
|
-
.requiredOption('-c, --toolchain-crn <crn>', 'The CRN of the source toolchain to check')
|
|
21
|
-
.option('-a --apikey <api key>', 'IBM Cloud IAM API key with permissions to read the toolchain.')
|
|
22
|
-
.showHelpAfterError()
|
|
23
|
-
.hook('preAction', cmd => cmd.showHelpAfterError(false)) // only show help during validation
|
|
24
|
-
.action(main);
|
|
25
|
-
|
|
26
|
-
async function main(options) {
|
|
27
|
-
const toolchainCrn = options.toolchainCrn;
|
|
28
|
-
const apiKey = options.apikey || parseEnvVar('IBMCLOUD_API_KEY');
|
|
29
|
-
|
|
30
|
-
if (!apiKey) {
|
|
31
|
-
logger.error('Missing IBM Cloud IAM API key', LOG_STAGES.setup);
|
|
32
|
-
exit(1);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
logger.print(`Checking secrets for toolchain ${toolchainCrn}...`);
|
|
36
|
-
|
|
37
|
-
const decomposedCrn = decomposeCrn(toolchainCrn);
|
|
38
|
-
|
|
39
|
-
const token = await getBearerToken(apiKey);
|
|
40
|
-
const toolchainId = decomposedCrn.serviceInstance;
|
|
41
|
-
const region = decomposedCrn.location;
|
|
42
|
-
|
|
43
|
-
const getToolsRes = await getToolchainTools(token, toolchainId, region);
|
|
44
|
-
|
|
45
|
-
const toolResults = [];
|
|
46
|
-
const pipelineResults = [];
|
|
47
|
-
|
|
48
|
-
if (getToolsRes?.tools?.length > 0) {
|
|
49
|
-
for (let i = 0; i < getToolsRes.tools.length; i++) {
|
|
50
|
-
const tool = getToolsRes.tools[i];
|
|
51
|
-
|
|
52
|
-
// Skip iff it's GitHub/GitLab/GRIT integration with OAuth
|
|
53
|
-
if (['githubconsolidated', 'github_integrated', 'gitlab', 'hostedgit'].includes(tool.tool_type_id) && (tool.parameters?.auth_type === '' || tool.parameters?.auth_type === 'oauth'))
|
|
54
|
-
continue;
|
|
55
|
-
|
|
56
|
-
// Check tool integrations for any plain text secret values
|
|
57
|
-
if (SECRET_KEYS_MAP[tool.tool_type_id]) {
|
|
58
|
-
SECRET_KEYS_MAP[tool.tool_type_id].forEach((entry) => {
|
|
59
|
-
const updateableSecretParam = entry.key;
|
|
60
|
-
if (tool.parameters[updateableSecretParam] && !isSecretReference(tool.parameters[updateableSecretParam]) && tool.parameters[updateableSecretParam].length > 0) {
|
|
61
|
-
toolResults.push({
|
|
62
|
-
'Tool ID': tool.id,
|
|
63
|
-
'Tool Type': tool.tool_type_id,
|
|
64
|
-
'Property Name': updateableSecretParam
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// For tekton pipelines, check for any plain text secret properties
|
|
71
|
-
if (tool.tool_type_id === 'pipeline' && tool.parameters?.type === 'tekton') {
|
|
72
|
-
const pipelineData = await getPipelineData(token, tool.id, region);
|
|
73
|
-
|
|
74
|
-
pipelineData?.properties.forEach((prop) => {
|
|
75
|
-
if (prop.type === 'secure' && !isSecretReference(prop.value) && prop.value.length > 0) {
|
|
76
|
-
pipelineResults.push({
|
|
77
|
-
'Pipeline ID': pipelineData.id,
|
|
78
|
-
'Trigger Name': '-',
|
|
79
|
-
'Property Name': prop.name
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
pipelineData?.triggers.forEach((trigger) => {
|
|
85
|
-
trigger.properties?.forEach((prop) => {
|
|
86
|
-
if (prop.type === 'secure' && !isSecretReference(prop.value) && prop.value.length > 0) {
|
|
87
|
-
pipelineResults.push({
|
|
88
|
-
'Pipeline ID': pipelineData.id,
|
|
89
|
-
'Trigger Name': trigger.name,
|
|
90
|
-
'Property Name': prop.name
|
|
91
|
-
});
|
|
92
|
-
};
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
if (toolResults.length > 0) {
|
|
100
|
-
logger.print();
|
|
101
|
-
logger.print('The following plain text properties were found in tool integrations bound to the toolchain:');
|
|
102
|
-
logger.table(toolResults);
|
|
103
|
-
};
|
|
104
|
-
if (pipelineResults.length > 0) {
|
|
105
|
-
logger.print();
|
|
106
|
-
logger.print('The following plain text properties were found in Tekton pipeline(s) bound to the toolchain:');
|
|
107
|
-
logger.table(pipelineResults);
|
|
108
|
-
};
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export default command;
|