@ibm-cloud/cd-tools 1.1.1 → 1.2.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/README.md +4 -4
- package/cmd/check-secrets.js +13 -12
- package/cmd/copy-toolchain.js +48 -25
- package/cmd/utils/import-terraform.js +333 -0
- package/cmd/utils/logger.js +9 -6
- package/cmd/utils/requests.js +51 -6
- package/cmd/utils/terraform.js +12 -33
- package/cmd/utils/utils.js +31 -6
- package/cmd/utils/validate.js +25 -63
- package/config.js +60 -29
- package/package.json +11 -2
- package/test/README.md +40 -0
- package/test/config/local.template.json +8 -0
- package/test/copy-toolchain/import.test.js +11 -0
- package/test/copy-toolchain/terraform.test.js +11 -0
- package/test/copy-toolchain/validation.test.js +126 -0
- package/test/data/mocks.js +29 -0
- package/test/data/test-toolchains.js +21 -0
- package/test/setup.js +46 -0
- package/test/utils/testUtils.js +137 -0
package/README.md
CHANGED
|
@@ -22,7 +22,6 @@ Provides tools to work with IBM Cloud Continuous Delivery resources, including *
|
|
|
22
22
|
## Prerequisites
|
|
23
23
|
- Node.js v20 (or later)
|
|
24
24
|
- Terraform v1.13.3 (or later)
|
|
25
|
-
- Terraformer v0.8.30 (or later)
|
|
26
25
|
- An **IBM Cloud API key** with the following IAM access permissions:
|
|
27
26
|
- **Viewer** for the source Toolchain(s) being copied
|
|
28
27
|
- **Editor** for create new Toolchains in the target region
|
|
@@ -30,20 +29,18 @@ Provides tools to work with IBM Cloud Continuous Delivery resources, including *
|
|
|
30
29
|
- For Git Repos and Issue Tracking projects, Personal Access Tokens (PAT) for the source and destination regions are required, with the `api` scope.
|
|
31
30
|
|
|
32
31
|
## Install
|
|
33
|
-
### Install Node.js, Terraform
|
|
32
|
+
### Install Node.js, Terraform
|
|
34
33
|
|
|
35
34
|
#### MacOS
|
|
36
35
|
```sh
|
|
37
36
|
brew install node
|
|
38
37
|
brew tap hashicorp/tap
|
|
39
38
|
brew install hashicorp/tap/terraform
|
|
40
|
-
brew install terraformer
|
|
41
39
|
```
|
|
42
40
|
|
|
43
41
|
#### Other platfoms
|
|
44
42
|
- Node.js [install instructions](https://nodejs.org/en/download)
|
|
45
43
|
- Terraform [install instructions](https://developer.hashicorp.com/terraform/install)
|
|
46
|
-
- Terraformer [install instructions](https://github.com/GoogleCloudPlatform/terraformer?tab=readme-ov-file#installation)
|
|
47
44
|
|
|
48
45
|
## Usage
|
|
49
46
|
|
|
@@ -65,3 +62,6 @@ Commands:
|
|
|
65
62
|
copy-toolchain [options] Copies a toolchain, including tool integrations and Tekton pipelines, to another region or resource group.
|
|
66
63
|
help [command] display help for command
|
|
67
64
|
```
|
|
65
|
+
|
|
66
|
+
## Test
|
|
67
|
+
All test setup and usage instructions are documented in [test/README.md](./test/README.md).
|
package/cmd/check-secrets.js
CHANGED
|
@@ -13,15 +13,15 @@ import { Command } from 'commander';
|
|
|
13
13
|
import { parseEnvVar, decomposeCrn, isSecretReference } from './utils/utils.js';
|
|
14
14
|
import { logger, LOG_STAGES } from './utils/logger.js';
|
|
15
15
|
import { getBearerToken, getToolchainTools, getPipelineData } from './utils/requests.js';
|
|
16
|
-
import {
|
|
16
|
+
import { SECRET_KEYS_MAP } from '../config.js';
|
|
17
17
|
|
|
18
18
|
const command = new Command('check-secrets')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
25
|
|
|
26
26
|
async function main(options) {
|
|
27
27
|
const toolchainCrn = options.toolchainCrn;
|
|
@@ -50,8 +50,9 @@ async function main(options) {
|
|
|
50
50
|
const tool = getToolsRes.tools[i];
|
|
51
51
|
|
|
52
52
|
// Check tool integrations for any plain text secret values
|
|
53
|
-
if (
|
|
54
|
-
|
|
53
|
+
if (SECRET_KEYS_MAP[tool.tool_type_id]) {
|
|
54
|
+
SECRET_KEYS_MAP[tool.tool_type_id].forEach((entry) => {
|
|
55
|
+
const updateableSecretParam = entry.key;
|
|
55
56
|
if (tool.parameters[updateableSecretParam] && !isSecretReference(tool.parameters[updateableSecretParam])) {
|
|
56
57
|
toolResults.push({
|
|
57
58
|
'Tool ID': tool.id,
|
|
@@ -64,13 +65,13 @@ async function main(options) {
|
|
|
64
65
|
|
|
65
66
|
// For tekton pipelines, check for any plain text secret properties
|
|
66
67
|
if (tool.tool_type_id === 'pipeline' && tool.parameters?.type === 'tekton') {
|
|
67
|
-
const pipelineData = await getPipelineData
|
|
68
|
+
const pipelineData = await getPipelineData(token, tool.id, region);
|
|
68
69
|
|
|
69
70
|
pipelineData?.properties.forEach((prop) => {
|
|
70
71
|
if (prop.type === 'secure' && !isSecretReference(prop.value)) {
|
|
71
72
|
pipelineResults.push({
|
|
72
73
|
'Pipeline ID': pipelineData.id,
|
|
73
|
-
'Trigger Name': '-',
|
|
74
|
+
'Trigger Name': '-',
|
|
74
75
|
'Property Name': prop.name
|
|
75
76
|
});
|
|
76
77
|
};
|
|
@@ -81,7 +82,7 @@ async function main(options) {
|
|
|
81
82
|
if (prop.type === 'secure' && !isSecretReference(prop.value)) {
|
|
82
83
|
pipelineResults.push({
|
|
83
84
|
'Pipeline ID': pipelineData.id,
|
|
84
|
-
'Trigger Name': trigger.name,
|
|
85
|
+
'Trigger Name': trigger.name,
|
|
85
86
|
'Property Name': prop.name
|
|
86
87
|
});
|
|
87
88
|
};
|
package/cmd/copy-toolchain.js
CHANGED
|
@@ -15,9 +15,10 @@ import { Command, Option } from 'commander';
|
|
|
15
15
|
|
|
16
16
|
import { parseEnvVar } from './utils/utils.js';
|
|
17
17
|
import { logger, LOG_STAGES } from './utils/logger.js';
|
|
18
|
-
import {
|
|
19
|
-
import { getAccountId, getBearerToken,
|
|
20
|
-
import { validatePrereqsVersions,
|
|
18
|
+
import { setTerraformEnv, initProviderFile, setupTerraformFiles, runTerraformInit, getNumResourcesPlanned, runTerraformApply, getNumResourcesCreated, getNewToolchainId } from './utils/terraform.js';
|
|
19
|
+
import { getAccountId, getBearerToken, getIamAuthPolicies, getResourceGroupIdAndName, getToolchain } from './utils/requests.js';
|
|
20
|
+
import { validatePrereqsVersions, validateTag, validateToolchainId, validateToolchainName, validateTools, validateOAuth, warnDuplicateName, validateGritUrl } from './utils/validate.js';
|
|
21
|
+
import { importTerraform } from './utils/import-terraform.js';
|
|
21
22
|
|
|
22
23
|
import { COPY_TOOLCHAIN_DESC, MIGRATION_DOC_URL, TARGET_REGIONS, SOURCE_REGIONS } from '../config.js';
|
|
23
24
|
|
|
@@ -27,7 +28,8 @@ process.on('exit', (code) => {
|
|
|
27
28
|
|
|
28
29
|
const LOGS_DIR = '.logs';
|
|
29
30
|
const TEMP_DIR = '.migration-temp'
|
|
30
|
-
const
|
|
31
|
+
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
|
|
32
|
+
const DEBUG_MODE = process.env['DEBUG_MODE'] === 'true' ? true : false; // when true, temp folder is preserved
|
|
31
33
|
const OUTPUT_DIR = 'output-' + new Date().getTime();
|
|
32
34
|
const DRY_RUN = false; // when true, terraform apply does not run
|
|
33
35
|
|
|
@@ -74,7 +76,7 @@ async function main(options) {
|
|
|
74
76
|
const verbosity = options.silent ? 0 : options.verbose ? 2 : 1;
|
|
75
77
|
|
|
76
78
|
logger.setVerbosity(verbosity);
|
|
77
|
-
logger.createLogStream(`${LOGS_DIR}/copy-toolchain-${new Date().getTime()}.log`);
|
|
79
|
+
if (LOG_DUMP) logger.createLogStream(`${LOGS_DIR}/copy-toolchain-${new Date().getTime()}.log`);
|
|
78
80
|
logger.dump(`Options: ${JSON.stringify(options)}\n`);
|
|
79
81
|
|
|
80
82
|
let bearer;
|
|
@@ -84,11 +86,13 @@ async function main(options) {
|
|
|
84
86
|
let targetToolchainName = options.name;
|
|
85
87
|
let targetTag = options.tag;
|
|
86
88
|
let targetRgId;
|
|
89
|
+
let targetRgName;
|
|
87
90
|
let apiKey = options.apikey;
|
|
91
|
+
let policyIds; // used to include s2s auth policies
|
|
88
92
|
let moreTfResources = {};
|
|
89
93
|
let gritMapping = {};
|
|
90
94
|
|
|
91
|
-
// Validate arguments are valid and check if
|
|
95
|
+
// Validate arguments are valid and check if Terraform is installed appropriately
|
|
92
96
|
try {
|
|
93
97
|
validatePrereqsVersions();
|
|
94
98
|
|
|
@@ -137,17 +141,11 @@ async function main(options) {
|
|
|
137
141
|
exit(1);
|
|
138
142
|
}
|
|
139
143
|
|
|
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
|
-
}
|
|
144
|
+
({ id: targetRgId, name: targetRgName } = await getResourceGroupIdAndName(bearer, accountId, targetRg || sourceToolchainData['resource_group_id']));
|
|
147
145
|
|
|
148
146
|
// reuse name if not provided
|
|
149
147
|
if (!targetToolchainName) targetToolchainName = sourceToolchainData['name'];
|
|
150
|
-
[targetToolchainName, targetTag] = await warnDuplicateName(bearer, accountId, targetToolchainName, sourceRegion, targetRegion, targetRgId, targetTag, skipUserConfirmation);
|
|
148
|
+
[targetToolchainName, targetTag] = await warnDuplicateName(bearer, accountId, targetToolchainName, sourceRegion, targetRegion, targetRgId, targetRgName, targetTag, skipUserConfirmation);
|
|
151
149
|
|
|
152
150
|
const allTools = await logger.withSpinner(validateTools,
|
|
153
151
|
'Validating Toolchain Tool(s)...',
|
|
@@ -173,6 +171,7 @@ async function main(options) {
|
|
|
173
171
|
// collect instances of legacy GHE tool integrations
|
|
174
172
|
const collectGHE = () => {
|
|
175
173
|
moreTfResources['github_integrated'] = [];
|
|
174
|
+
|
|
176
175
|
allTools.forEach((t) => {
|
|
177
176
|
if (t.tool_type_id === 'github_integrated') {
|
|
178
177
|
moreTfResources['github_integrated'].push(t);
|
|
@@ -182,6 +181,26 @@ async function main(options) {
|
|
|
182
181
|
|
|
183
182
|
collectGHE();
|
|
184
183
|
|
|
184
|
+
const collectPolicyIds = async () => {
|
|
185
|
+
moreTfResources['iam_authorization_policy'] = [];
|
|
186
|
+
|
|
187
|
+
const res = await getIamAuthPolicies(bearer, accountId);
|
|
188
|
+
|
|
189
|
+
policyIds = res['policies'].filter((p) => p.subjects[0].attributes.find(
|
|
190
|
+
(a) => a.name === 'serviceInstance' && a.value === sourceToolchainId)
|
|
191
|
+
);
|
|
192
|
+
policyIds = policyIds.map((p) => p.id);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (includeS2S) {
|
|
196
|
+
try {
|
|
197
|
+
collectPolicyIds();
|
|
198
|
+
} catch (e) {
|
|
199
|
+
logger.error('Something went wrong while fetching service-to-service auth policies', LOG_STAGES.setup);
|
|
200
|
+
throw e;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
185
204
|
logger.info('Arguments and required packages verified, proceeding with copying toolchain...', LOG_STAGES.setup);
|
|
186
205
|
|
|
187
206
|
// Set up temp folder
|
|
@@ -199,24 +218,28 @@ async function main(options) {
|
|
|
199
218
|
}
|
|
200
219
|
|
|
201
220
|
try {
|
|
202
|
-
|
|
221
|
+
let nonSecretRefs;
|
|
222
|
+
|
|
223
|
+
const importTerraformWrapper = async () => {
|
|
203
224
|
setTimeout(() => {
|
|
204
225
|
logger.updateSpinnerMsg('Still importing toolchain...');
|
|
205
226
|
}, 5000);
|
|
206
227
|
|
|
207
|
-
setTerraformerEnv(apiKey, sourceToolchainId, includeS2S);
|
|
208
|
-
|
|
209
228
|
await initProviderFile(sourceRegion, TEMP_DIR);
|
|
210
229
|
await runTerraformInit(TEMP_DIR);
|
|
211
230
|
|
|
212
|
-
await
|
|
231
|
+
nonSecretRefs = await importTerraform(bearer, apiKey, sourceRegion, sourceToolchainId, targetToolchainName, policyIds, TEMP_DIR, isCompact, verbosity);
|
|
213
232
|
};
|
|
233
|
+
|
|
214
234
|
await logger.withSpinner(
|
|
215
|
-
|
|
235
|
+
importTerraformWrapper,
|
|
216
236
|
'Importing toolchain...',
|
|
217
|
-
'Toolchain successfully imported
|
|
218
|
-
LOG_STAGES.
|
|
237
|
+
'Toolchain successfully imported',
|
|
238
|
+
LOG_STAGES.import
|
|
219
239
|
);
|
|
240
|
+
|
|
241
|
+
if (nonSecretRefs.length > 0) logger.warn(`\nWarning! The following generated terraform resource contains a hashed secret, applying without changes may result in error(s):\n${nonSecretRefs.map((entry) => `- ${entry}\n`).join('')}`, '', true);
|
|
242
|
+
|
|
220
243
|
} catch (err) {
|
|
221
244
|
if (err.message && err.stack) {
|
|
222
245
|
const errMsg = verbosity > 1 ? err.stack : err.message;
|
|
@@ -229,10 +252,10 @@ async function main(options) {
|
|
|
229
252
|
// Prepare for Terraform
|
|
230
253
|
try {
|
|
231
254
|
if (!fs.existsSync(outputDir)) {
|
|
232
|
-
logger.info(`Creating output directory "${outputDir}"...`, LOG_STAGES.
|
|
255
|
+
logger.info(`Creating output directory "${outputDir}"...`, LOG_STAGES.import);
|
|
233
256
|
fs.mkdirSync(outputDir);
|
|
234
257
|
} else {
|
|
235
|
-
logger.info(`Output directory "${outputDir}" already exists`, LOG_STAGES.
|
|
258
|
+
logger.info(`Output directory "${outputDir}" already exists`, LOG_STAGES.import);
|
|
236
259
|
}
|
|
237
260
|
|
|
238
261
|
await setupTerraformFiles({
|
|
@@ -253,7 +276,7 @@ async function main(options) {
|
|
|
253
276
|
} catch (err) {
|
|
254
277
|
if (err.message && err.stack) {
|
|
255
278
|
const errMsg = verbosity > 1 ? err.stack : err.message;
|
|
256
|
-
logger.error(errMsg, LOG_STAGES.
|
|
279
|
+
logger.error(errMsg, LOG_STAGES.import);
|
|
257
280
|
}
|
|
258
281
|
await handleCleanup();
|
|
259
282
|
exit(1);
|
|
@@ -262,7 +285,7 @@ async function main(options) {
|
|
|
262
285
|
// Run Terraform
|
|
263
286
|
try {
|
|
264
287
|
if (!dryRun) {
|
|
265
|
-
setTerraformEnv(verbosity);
|
|
288
|
+
setTerraformEnv(apiKey, verbosity);
|
|
266
289
|
|
|
267
290
|
await logger.withSpinner(runTerraformInit,
|
|
268
291
|
'Running terraform init...',
|
|
@@ -0,0 +1,333 @@
|
|
|
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
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
|
|
13
|
+
import { parse as tfToJson } from '@cdktf/hcl2json'
|
|
14
|
+
import { jsonToTf } from 'json-to-tf';
|
|
15
|
+
|
|
16
|
+
import { getPipelineData, getToolchainTools } from './requests.js';
|
|
17
|
+
import { runTerraformPlanGenerate, setTerraformEnv } from './terraform.js';
|
|
18
|
+
import { getRandChars, isSecretReference, normalizeName } from './utils.js';
|
|
19
|
+
|
|
20
|
+
import { SECRET_KEYS_MAP, SUPPORTED_TOOLS_MAP } from '../../config.js';
|
|
21
|
+
|
|
22
|
+
const writeFilePromise = promisify(fs.writeFile);
|
|
23
|
+
|
|
24
|
+
export async function importTerraform(token, apiKey, region, toolchainId, toolchainName, policyIds, dir, isCompact, verbosity) {
|
|
25
|
+
// STEP 1/2: set up terraform file with import blocks
|
|
26
|
+
const importBlocks = []; // an array of objects representing import blocks, used in importBlocksToTf
|
|
27
|
+
const additionalProps = {}; // maps resource name to array of { property/param, value }, used to override terraform import
|
|
28
|
+
|
|
29
|
+
const toolIdMap = {}; // maps tool ids to { type, name }, used to add references
|
|
30
|
+
|
|
31
|
+
const repoUrlMap = {}; // maps repo urls to { type, name }, used to add references
|
|
32
|
+
const repoResources = [
|
|
33
|
+
'ibm_cd_toolchain_tool_bitbucketgit',
|
|
34
|
+
'ibm_cd_toolchain_tool_hostedgit',
|
|
35
|
+
'ibm_cd_toolchain_tool_gitlab',
|
|
36
|
+
'ibm_cd_toolchain_tool_githubconsolidated'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const nonSecretRefs = [];
|
|
40
|
+
|
|
41
|
+
let block = importBlock(toolchainId, toolchainName, 'ibm_cd_toolchain');
|
|
42
|
+
importBlocks.push(block);
|
|
43
|
+
|
|
44
|
+
const toolchainResName = block.name;
|
|
45
|
+
let pipelineResName;
|
|
46
|
+
|
|
47
|
+
// get list of tools
|
|
48
|
+
const allTools = await getToolchainTools(token, toolchainId, region);
|
|
49
|
+
for (const tool of allTools.tools) {
|
|
50
|
+
const toolName = tool.parameters?.name ?? tool.tool_type_id;
|
|
51
|
+
|
|
52
|
+
if (tool.tool_type_id in SUPPORTED_TOOLS_MAP) {
|
|
53
|
+
block = importBlock(`${toolchainId}/${tool.id}`, toolName, SUPPORTED_TOOLS_MAP[tool.tool_type_id]);
|
|
54
|
+
importBlocks.push(block);
|
|
55
|
+
|
|
56
|
+
const toolResName = block.name;
|
|
57
|
+
pipelineResName = block.name; // used below
|
|
58
|
+
|
|
59
|
+
toolIdMap[tool.id] = { type: SUPPORTED_TOOLS_MAP[tool.tool_type_id], name: toolResName };
|
|
60
|
+
|
|
61
|
+
// overwrite hard-coded id with reference
|
|
62
|
+
additionalProps[block.name] = [
|
|
63
|
+
{ property: 'toolchain_id', value: `\${ibm_cd_toolchain.${toolchainResName}.id}` },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// check and add secret refs
|
|
67
|
+
if (tool.tool_type_id in SECRET_KEYS_MAP) {
|
|
68
|
+
SECRET_KEYS_MAP[tool.tool_type_id].forEach(({ key, tfKey, prereq, required }) => {
|
|
69
|
+
if (prereq) {
|
|
70
|
+
if (!prereq.values.includes(tool[prereq.key])) return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isSecretReference(tool.parameters[key])) {
|
|
74
|
+
additionalProps[block.name].push({ param: tfKey, value: tool.parameters[key] });
|
|
75
|
+
} else {
|
|
76
|
+
nonSecretRefs.push(block.name);
|
|
77
|
+
if (required) additionalProps[block.name].push({ param: tfKey, value: `<${tfKey}>` });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (tool.tool_type_id === 'pipeline' && tool.parameters?.type === 'tekton') {
|
|
84
|
+
const pipelineData = await getPipelineData(token, tool.id, region);
|
|
85
|
+
|
|
86
|
+
block = importBlock(pipelineData.id, toolName, 'ibm_cd_tekton_pipeline');
|
|
87
|
+
importBlocks.push(block);
|
|
88
|
+
|
|
89
|
+
// overwrite hard-coded id with reference
|
|
90
|
+
additionalProps[block.name] = [
|
|
91
|
+
{ property: 'pipeline_id', value: `\${ibm_cd_toolchain_tool_pipeline.${pipelineResName}.tool_id}` },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
pipelineData.definitions.forEach((def) => {
|
|
96
|
+
block = importBlock(`${pipelineData.id}/${def.id}`, 'definition', 'ibm_cd_tekton_pipeline_definition');
|
|
97
|
+
importBlocks.push(block);
|
|
98
|
+
|
|
99
|
+
// overwrite hard-coded id with reference
|
|
100
|
+
additionalProps[block.name] = [
|
|
101
|
+
{ property: 'pipeline_id', value: `\${ibm_cd_toolchain_tool_pipeline.${pipelineResName}.tool_id}` },
|
|
102
|
+
];
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
pipelineData.properties.forEach((prop) => {
|
|
106
|
+
block = importBlock(`${pipelineData.id}/${prop.name}`, prop.name, 'ibm_cd_tekton_pipeline_property');
|
|
107
|
+
importBlocks.push(block);
|
|
108
|
+
|
|
109
|
+
// overwrite hard-coded id with reference
|
|
110
|
+
additionalProps[block.name] = [
|
|
111
|
+
{ property: 'pipeline_id', value: `\${ibm_cd_toolchain_tool_pipeline.${pipelineResName}.tool_id}` },
|
|
112
|
+
];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
pipelineData.triggers.forEach((trig) => {
|
|
116
|
+
block = importBlock(`${pipelineData.id}/${trig.id}`, trig.name, 'ibm_cd_tekton_pipeline_trigger');
|
|
117
|
+
importBlocks.push(block);
|
|
118
|
+
|
|
119
|
+
// overwrite hard-coded id with reference
|
|
120
|
+
additionalProps[block.name] = [
|
|
121
|
+
{ property: 'pipeline_id', value: `\${ibm_cd_toolchain_tool_pipeline.${pipelineResName}.tool_id}` },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const triggerResName = block.name;
|
|
125
|
+
|
|
126
|
+
trig.properties.forEach((trigProp) => {
|
|
127
|
+
block = importBlock(`${pipelineData.id}/${trig.id}/${trigProp.name}`, trigProp.name, 'ibm_cd_tekton_pipeline_trigger_property');
|
|
128
|
+
importBlocks.push(block);
|
|
129
|
+
|
|
130
|
+
// overwrite hard-coded id with reference
|
|
131
|
+
additionalProps[block.name] = [
|
|
132
|
+
{ property: 'pipeline_id', value: `\${ibm_cd_toolchain_tool_pipeline.${pipelineResName}.tool_id}` },
|
|
133
|
+
{ property: 'trigger_id', value: `\${ibm_cd_tekton_pipeline_trigger.${triggerResName}.trigger_id}` }
|
|
134
|
+
];
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// include s2s
|
|
141
|
+
if (policyIds) {
|
|
142
|
+
for (const policyId of policyIds) {
|
|
143
|
+
block = importBlock(policyId, 'iam_authorization_policy', 'ibm_iam_authorization_policy');
|
|
144
|
+
importBlocks.push(block);
|
|
145
|
+
|
|
146
|
+
// overwrite hard-coded id with reference
|
|
147
|
+
additionalProps[block.name] = [
|
|
148
|
+
{ property: 'source_resource_instance_id', value: `\${ibm_cd_toolchain.${toolchainResName}.id}` },
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await importBlocksToTf(importBlocks, dir);
|
|
154
|
+
|
|
155
|
+
if (!fs.existsSync(`${dir}/generated`)) fs.mkdirSync(`${dir}/generated`);
|
|
156
|
+
|
|
157
|
+
// STEP 2/2: run terraform import and post-processing
|
|
158
|
+
setTerraformEnv(apiKey, verbosity);
|
|
159
|
+
await runTerraformPlanGenerate(dir, 'generated/draft.tf').catch(() => { }); // temp fix for errors due to bugs in the provider
|
|
160
|
+
|
|
161
|
+
const generatedFile = fs.readFileSync(`${dir}/generated/draft.tf`);
|
|
162
|
+
const generatedFileJson = await tfToJson('draft.tf', generatedFile.toString());
|
|
163
|
+
|
|
164
|
+
const newTfFileObj = { 'resource': {} }
|
|
165
|
+
|
|
166
|
+
for (const [key, value] of Object.entries(generatedFileJson['resource'])) {
|
|
167
|
+
for (const [k, v] of Object.entries(value)) {
|
|
168
|
+
newTfFileObj['resource'][key] = { ...(newTfFileObj['resource'][key] ?? []), [k]: v[0] };
|
|
169
|
+
|
|
170
|
+
// remove empty tool, which breaks jsonToTf
|
|
171
|
+
try {
|
|
172
|
+
if (Object.keys(newTfFileObj['resource'][key][k]['source'][0]['properties'][0]['tool'][0]).length < 1) {
|
|
173
|
+
delete newTfFileObj['resource'][key][k]['source'][0]['properties'][0]['tool'];
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// do nothing
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ignore null values
|
|
180
|
+
for (const [k2, v2] of Object.entries(v[0])) {
|
|
181
|
+
if (v2 === null) delete newTfFileObj['resource'][key][k][k2];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ignore null values in parameters
|
|
185
|
+
try {
|
|
186
|
+
if (Object.keys(v[0]['parameters'][0]).length > 0) {
|
|
187
|
+
for (const [k2, v2] of Object.entries(v[0]['parameters'][0])) {
|
|
188
|
+
if (v2 === null) delete newTfFileObj['resource'][key][k]['parameters'][0][k2];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// do nothing
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ignore null values in source properties
|
|
196
|
+
try {
|
|
197
|
+
if (Object.keys(v[0]['source'][0]['properties'][0]).length > 0) {
|
|
198
|
+
for (const [k2, v2] of Object.entries(v[0]['source'][0]['properties'][0])) {
|
|
199
|
+
if (v2 === null) delete newTfFileObj['resource'][key][k]['source'][0]['properties'][0][k2];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// do nothing
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// add/overwrite additional props
|
|
207
|
+
if (k in additionalProps) {
|
|
208
|
+
additionalProps[k].forEach(({ param, property, value }) => {
|
|
209
|
+
if (property) newTfFileObj['resource'][key][k][property] = value;
|
|
210
|
+
if (param) {
|
|
211
|
+
newTfFileObj['resource'][key][k]['parameters'][0][param] = value;
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// add relevent references and depends_on
|
|
217
|
+
if (key === 'ibm_cd_tekton_pipeline') {
|
|
218
|
+
const workerId = newTfFileObj['resource'][key][k]['worker'][0]['id'];
|
|
219
|
+
if (workerId != null && workerId != 'public' && workerId in toolIdMap) {
|
|
220
|
+
newTfFileObj['resource'][key][k]['worker'][0]['id'] = `\${${toolIdMap[workerId].type}.${toolIdMap[workerId].name}.tool_id}`;
|
|
221
|
+
}
|
|
222
|
+
} else if (key === 'ibm_cd_tekton_pipeline_property' || key === 'ibm_cd_tekton_pipeline_trigger_property') {
|
|
223
|
+
const propValue = newTfFileObj['resource'][key][k]['value'];
|
|
224
|
+
if (newTfFileObj['resource'][key][k]['type'] === 'integration' && propValue in toolIdMap) {
|
|
225
|
+
newTfFileObj['resource'][key][k]['depends_on'] = [`\${${toolIdMap[propValue].type}.${toolIdMap[propValue].name}}`];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// clean up unused/misplaced params
|
|
230
|
+
if (key === 'ibm_iam_authorization_policy') {
|
|
231
|
+
const deleteKeys = [
|
|
232
|
+
'subject_attributes',
|
|
233
|
+
'resource_attributes',
|
|
234
|
+
'source_service_account',
|
|
235
|
+
'transaction_id'
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
for (const toDelete of deleteKeys) {
|
|
239
|
+
delete newTfFileObj['resource'][key][k][toDelete];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (repoResources.includes(key)) {
|
|
244
|
+
const paramsMap = newTfFileObj['resource'][key][k]['parameters'][0];
|
|
245
|
+
|
|
246
|
+
// collect repo url references to be added on second pass
|
|
247
|
+
const repoUrl = paramsMap['repo_url'];
|
|
248
|
+
repoUrlMap[repoUrl] = { type: key, name: k };
|
|
249
|
+
|
|
250
|
+
// set up initialization
|
|
251
|
+
const initializationMap = {
|
|
252
|
+
git_id: paramsMap['git_id'],
|
|
253
|
+
type: paramsMap['type'],
|
|
254
|
+
repo_url: paramsMap['repo_url'],
|
|
255
|
+
private_repo: paramsMap['private_repo'],
|
|
256
|
+
};
|
|
257
|
+
newTfFileObj['resource'][key][k]['initialization'] = [initializationMap];
|
|
258
|
+
|
|
259
|
+
// clean up parameters
|
|
260
|
+
const newParamsMap = {};
|
|
261
|
+
const paramsToInclude = ['api_token', 'auth_type', 'enable_traceability', 'integration_owner', 'toolchain_issues_enabled'];
|
|
262
|
+
for (const param of paramsToInclude) {
|
|
263
|
+
newParamsMap[param] = paramsMap[param];
|
|
264
|
+
}
|
|
265
|
+
newTfFileObj['resource'][key][k]['parameters'][0] = newParamsMap;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// add repo url depends_on on second pass
|
|
271
|
+
for (const [key, value] of Object.entries(generatedFileJson['resource'])) {
|
|
272
|
+
for (const [k, _] of Object.entries(value)) {
|
|
273
|
+
if (key === 'ibm_cd_tekton_pipeline_definition' || key === 'ibm_cd_tekton_pipeline_trigger') {
|
|
274
|
+
try {
|
|
275
|
+
const thisUrl = newTfFileObj['resource'][key][k]['source'][0]['properties'][0]['url'];
|
|
276
|
+
|
|
277
|
+
if (thisUrl in repoUrlMap) {
|
|
278
|
+
newTfFileObj['resource'][key][k]['depends_on'] = [`\${${repoUrlMap[thisUrl].type}.${repoUrlMap[thisUrl].name}}`];
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
// do nothing
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!isCompact) {
|
|
288
|
+
for (const [key, value] of Object.entries(newTfFileObj['resource'])) {
|
|
289
|
+
if (!key.startsWith('ibm_')) continue;
|
|
290
|
+
const newFileName = key.split('ibm_')[1];
|
|
291
|
+
|
|
292
|
+
const newFileContents = { 'resource': { [key]: value } };
|
|
293
|
+
const newFileContentsJson = jsonToTf(JSON.stringify(newFileContents));
|
|
294
|
+
|
|
295
|
+
fs.writeFileSync(`${dir}/generated/${newFileName}.tf`, newFileContentsJson);
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
const generatedFileNew = jsonToTf(JSON.stringify(newTfFileObj));
|
|
299
|
+
fs.writeFileSync(`${dir}/generated/generated.tf`, generatedFileNew);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// remove draft
|
|
303
|
+
if (fs.existsSync(`${dir}/generated/draft.tf`)) fs.rmSync(`${dir}/generated/draft.tf`, { recursive: true });
|
|
304
|
+
|
|
305
|
+
return nonSecretRefs;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// objects have two keys, "id" and "to"
|
|
309
|
+
// e.g. { id: 'bc3d05f1-e6f7-4b5e-8647-8119d8037039', to: 'ibm_cd_toolchain.my_everything_toolchain_e22c' }
|
|
310
|
+
function importBlock(id, name, resourceType) {
|
|
311
|
+
const newName = `${normalizeName(name)}_${getRandChars(4)}`;
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
id: id,
|
|
315
|
+
to: `${resourceType}.${newName}`,
|
|
316
|
+
name: newName
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// importBlocks array to tf file
|
|
321
|
+
async function importBlocksToTf(blocks, dir) {
|
|
322
|
+
let fileContent = '';
|
|
323
|
+
|
|
324
|
+
blocks.forEach((block) => {
|
|
325
|
+
const template = `import {
|
|
326
|
+
id = "${block.id}"
|
|
327
|
+
to = ${block.to}
|
|
328
|
+
}\n\n`;
|
|
329
|
+
fileContent += template;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return await writeFilePromise(`${dir}/import.tf`, fileContent);
|
|
333
|
+
}
|
package/cmd/utils/logger.js
CHANGED
|
@@ -14,6 +14,8 @@ import stripAnsi from 'strip-ansi';
|
|
|
14
14
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
|
|
17
|
+
const DISABLE_SPINNER = process.env.DISABLE_SPINNER === 'true';
|
|
18
|
+
|
|
17
19
|
const COLORS = {
|
|
18
20
|
reset: '\x1b[0m',
|
|
19
21
|
gray: '\x1b[90m',
|
|
@@ -84,14 +86,15 @@ class Logger {
|
|
|
84
86
|
|
|
85
87
|
close() {
|
|
86
88
|
return new Promise((resolve, reject) => {
|
|
87
|
-
this.logStream
|
|
88
|
-
this.logStream
|
|
89
|
-
this.logStream
|
|
89
|
+
if (!this.logStream) resolve();
|
|
90
|
+
this.logStream.on('finish', resolve);
|
|
91
|
+
this.logStream.on('error', reject);
|
|
92
|
+
this.logStream.end();
|
|
90
93
|
});
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
startSpinner(msg, prefix = '') {
|
|
94
|
-
if (this.verbosity < 1) return;
|
|
97
|
+
if (this.verbosity < 1 || DISABLE_SPINNER) return;
|
|
95
98
|
this.spinner = ora({
|
|
96
99
|
prefixText: this.#getFullPrefix(prefix),
|
|
97
100
|
text: msg
|
|
@@ -103,7 +106,7 @@ class Logger {
|
|
|
103
106
|
resetSpinner() { if (this.verbosity >= 1) this.spinner = null; }
|
|
104
107
|
|
|
105
108
|
async withSpinner(asyncFn, loadingMsg, successMsg, prefix, ...args) {
|
|
106
|
-
if (this.verbosity < 1) {
|
|
109
|
+
if (this.verbosity < 1 || DISABLE_SPINNER) {
|
|
107
110
|
try {
|
|
108
111
|
return await asyncFn(...args);
|
|
109
112
|
}
|
|
@@ -167,7 +170,7 @@ export const logger = new Logger();
|
|
|
167
170
|
|
|
168
171
|
export const LOG_STAGES = {
|
|
169
172
|
setup: 'setup',
|
|
170
|
-
|
|
173
|
+
import: 'import',
|
|
171
174
|
tf: 'terraform',
|
|
172
175
|
info: 'info'
|
|
173
176
|
};
|