@ibm-cloud/cd-tools 1.3.4 → 1.4.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.
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { exit } from 'node:process';
11
- import { resolve } from 'node:path'
11
+ import { resolve } from 'node:path';
12
12
  import fs from 'node:fs';
13
13
 
14
14
  import { Command, Option } from 'commander';
@@ -16,7 +16,7 @@ import { Command, Option } from 'commander';
16
16
  import { parseEnvVar } from './utils/utils.js';
17
17
  import { logger, LOG_STAGES } from './utils/logger.js';
18
18
  import { setTerraformEnv, initProviderFile, setupTerraformFiles, runTerraformInit, getNumResourcesPlanned, runTerraformApply, getNumResourcesCreated, getNewToolchainId } from './utils/terraform.js';
19
- import { getAccountId, getBearerToken, getCdInstanceByRegion, getIamAuthPolicies, getResourceGroupIdAndName, getToolchain } from './utils/requests.js';
19
+ import { getAccountId, getBearerToken, getCdInstanceByRegion, getResourceGroupIdAndName, getToolchain } from './utils/requests.js';
20
20
  import { validatePrereqsVersions, validateTag, validateToolchainId, validateToolchainName, validateTools, validateOAuth, warnDuplicateName, validateGritUrl } from './utils/validate.js';
21
21
  import { importTerraform } from './utils/import-terraform.js';
22
22
 
@@ -54,7 +54,7 @@ const command = new Command('copy-toolchain')
54
54
  .option('-d, --terraform-dir <path>', '(Optional) The target local directory to store the generated Terraform (.tf) files')
55
55
  .option('-D, --dry-run', '(Optional) Skip running terraform apply; only generate the Terraform (.tf) files')
56
56
  .option('-f, --force', '(Optional) Force the copy toolchain command to run without user confirmation')
57
- .option('-S, --skip-s2s', '(Optional) Skip importing toolchain-generated service-to-service authorizations')
57
+ .option('-S, --skip-s2s', '(Optional) Skip creating toolchain-generated service-to-service authorizations')
58
58
  .option('-T, --skip-disable-triggers', '(Optional) Skip disabling Tekton pipeline Git or timed triggers. Note: This may result in duplicate pipeline runs')
59
59
  .option('-C, --compact', '(Optional) Generate all resources in a single resources.tf file')
60
60
  .option('-v, --verbose', '(Optional) Increase log output')
@@ -92,7 +92,6 @@ async function main(options) {
92
92
  let targetRgId;
93
93
  let targetRgName;
94
94
  let apiKey = options.apikey;
95
- let policyIds; // used to include s2s auth policies
96
95
  let moreTfResources = {};
97
96
  let gritMapping = {};
98
97
 
@@ -195,26 +194,6 @@ async function main(options) {
195
194
 
196
195
  collectGHE();
197
196
 
198
- const collectPolicyIds = async () => {
199
- moreTfResources['iam_authorization_policy'] = [];
200
-
201
- const res = await getIamAuthPolicies(bearer, accountId);
202
-
203
- policyIds = res['policies'].filter((p) => p.subjects[0].attributes.find(
204
- (a) => a.name === 'serviceInstance' && a.value === sourceToolchainId)
205
- );
206
- policyIds = policyIds.map((p) => p.id);
207
- };
208
-
209
- if (includeS2S) {
210
- try {
211
- collectPolicyIds();
212
- } catch (e) {
213
- logger.error('Something went wrong while fetching service-to-service auth policies', LOG_STAGES.setup);
214
- throw e;
215
- }
216
- }
217
-
218
197
  logger.info('Arguments and required packages verified, proceeding with copying toolchain...', LOG_STAGES.setup);
219
198
 
220
199
  // Set up temp folder
@@ -231,6 +210,9 @@ async function main(options) {
231
210
  exit(1);
232
211
  }
233
212
 
213
+ let toolchainTfName; // to target creating toolchain first
214
+ let s2sAuthTools; // to create s2s auth with script
215
+
234
216
  try {
235
217
  let nonSecretRefs;
236
218
 
@@ -242,7 +224,7 @@ async function main(options) {
242
224
  await initProviderFile(sourceRegion, TEMP_DIR);
243
225
  await runTerraformInit(TEMP_DIR, verbosity);
244
226
 
245
- nonSecretRefs = await importTerraform(bearer, apiKey, sourceRegion, sourceToolchainId, targetToolchainName, policyIds, TEMP_DIR, isCompact, verbosity);
227
+ [toolchainTfName, nonSecretRefs, s2sAuthTools] = await importTerraform(bearer, apiKey, sourceRegion, sourceToolchainId, targetToolchainName, TEMP_DIR, isCompact, verbosity);
246
228
  };
247
229
 
248
230
  await logger.withSpinner(
@@ -286,7 +268,8 @@ async function main(options) {
286
268
  tempDir: TEMP_DIR,
287
269
  moreTfResources: moreTfResources,
288
270
  gritMapping: gritMapping,
289
- skipUserConfirmation: skipUserConfirmation
271
+ skipUserConfirmation: skipUserConfirmation,
272
+ includeS2S: includeS2S
290
273
  });
291
274
  } catch (err) {
292
275
  if (err.message && err.stack) {
@@ -317,6 +300,27 @@ async function main(options) {
317
300
 
318
301
  let applyErrors = false;
319
302
 
303
+ if (includeS2S) {
304
+ const s2sRequests = s2sAuthTools.map((item) => {
305
+ return {
306
+ parameters: item['parameters'],
307
+ serviceId: item.tool_type_id,
308
+ env_id: `ibm:yp:${targetRegion}`
309
+ };
310
+ });
311
+ fs.writeFileSync(resolve(`${outputDir}/create-s2s.json`), JSON.stringify(s2sRequests));
312
+
313
+ // copy script
314
+ fs.copyFileSync(resolve('create-s2s-script.js'), resolve(`${outputDir}/create-s2s-script.js`), fs.constants.COPYFILE_EXCL);
315
+ }
316
+
317
+ // create toolchain, which invokes script to create s2s if applicable
318
+ await runTerraformApply(true, outputDir, verbosity, `ibm_cd_toolchain.${toolchainTfName}`).catch((err) => {
319
+ logger.error(err, LOG_STAGES.tf);
320
+ applyErrors = true;
321
+ });
322
+
323
+ // create the rest
320
324
  await runTerraformApply(skipUserConfirmation, outputDir, verbosity).catch((err) => {
321
325
  logger.error(err, LOG_STAGES.tf);
322
326
  applyErrors = true;
@@ -18,7 +18,7 @@ import { getRandChars, isSecretReference, normalizeName } from './utils.js';
18
18
 
19
19
  import { SECRET_KEYS_MAP, SUPPORTED_TOOLS_MAP } from '../../config.js';
20
20
 
21
- export async function importTerraform(token, apiKey, region, toolchainId, toolchainName, policyIds, dir, isCompact, verbosity) {
21
+ export async function importTerraform(token, apiKey, region, toolchainId, toolchainName, dir, isCompact, verbosity) {
22
22
  // STEP 1/2: set up terraform file with import blocks
23
23
  const importBlocks = []; // an array of objects representing import blocks, used in importBlocksToTf
24
24
  const additionalProps = {}; // maps resource name to array of { property/param, value }, used to override terraform import
@@ -41,6 +41,14 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
41
41
  const toolchainResName = block.name;
42
42
  let pipelineResName;
43
43
 
44
+ const requiresS2S = [
45
+ 'ibm_cd_toolchain_tool_appconfig',
46
+ 'ibm_cd_toolchain_tool_eventnotifications',
47
+ 'ibm_cd_toolchain_tool_keyprotect',
48
+ 'ibm_cd_toolchain_tool_secretsmanager'
49
+ ];
50
+ let s2sAuthTools = [];
51
+
44
52
  // get list of tools
45
53
  const allTools = await getToolchainTools(token, toolchainId, region);
46
54
  for (const tool of allTools.tools) {
@@ -55,6 +63,10 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
55
63
 
56
64
  toolIdMap[tool.id] = { type: SUPPORTED_TOOLS_MAP[tool.tool_type_id], name: toolResName };
57
65
 
66
+ if (requiresS2S.includes(SUPPORTED_TOOLS_MAP[tool.tool_type_id])) {
67
+ s2sAuthTools.push(tool);
68
+ }
69
+
58
70
  // overwrite hard-coded id with reference
59
71
  additionalProps[block.name] = [
60
72
  { property: 'toolchain_id', value: `\${ibm_cd_toolchain.${toolchainResName}.id}` },
@@ -139,19 +151,6 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
139
151
  }
140
152
  }
141
153
 
142
- // include s2s
143
- if (policyIds) {
144
- for (const policyId of policyIds) {
145
- block = importBlock(policyId, 'iam_authorization_policy', 'ibm_iam_authorization_policy');
146
- importBlocks.push(block);
147
-
148
- // overwrite hard-coded id with reference
149
- additionalProps[block.name] = [
150
- { property: 'source_resource_instance_id', value: `\${ibm_cd_toolchain.${toolchainResName}.id}` },
151
- ];
152
- }
153
- }
154
-
155
154
  importBlocksToTf(importBlocks, dir);
156
155
 
157
156
  if (!fs.existsSync(`${dir}/generated`)) fs.mkdirSync(`${dir}/generated`);
@@ -313,7 +312,7 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
313
312
  // remove draft
314
313
  if (fs.existsSync(`${dir}/generated/draft.tf`)) fs.rmSync(`${dir}/generated/draft.tf`, { recursive: true });
315
314
 
316
- return nonSecretRefs;
315
+ return [toolchainResName, nonSecretRefs, s2sAuthTools];
317
316
  }
318
317
 
319
318
  // objects have two keys, "id" and "to"
@@ -373,27 +373,6 @@ async function getGritGroupProject(privToken, region, groupId, projectName) {
373
373
  }
374
374
  }
375
375
 
376
- async function getIamAuthPolicies(bearer, accountId) {
377
- const apiBaseUrl = 'https://iam.cloud.ibm.com/v1';
378
- const options = {
379
- url: apiBaseUrl + '/policies',
380
- method: 'GET',
381
- headers: {
382
- 'Authorization': `Bearer ${bearer}`,
383
- 'Content-Type': 'application/json',
384
- },
385
- params: { account_id: accountId, type: 'authorization' },
386
- validateStatus: () => true
387
- };
388
- const response = await axios(options);
389
- switch (response.status) {
390
- case 200:
391
- return response.data;
392
- default:
393
- throw Error('Get auth policies failed');
394
- }
395
- }
396
-
397
376
  async function deleteToolchain(bearer, toolchainId, region) {
398
377
  const apiBaseUrl = `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
399
378
  const options = {
@@ -430,6 +409,5 @@ export {
430
409
  getGritUserProject,
431
410
  getGritGroup,
432
411
  getGritGroupProject,
433
- getIamAuthPolicies,
434
412
  deleteToolchain
435
413
  }
@@ -50,7 +50,7 @@ async function initProviderFile(targetRegion, dir) {
50
50
  return writeFilePromise(`${dir}/provider.tf`, jsonToTf(newProviderTfStr));
51
51
  }
52
52
 
53
- async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag, targetToolchainName, targetRgId, disableTriggers, isCompact, outputDir, tempDir, moreTfResources, gritMapping, skipUserConfirmation }) {
53
+ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag, targetToolchainName, targetRgId, disableTriggers, isCompact, outputDir, tempDir, moreTfResources, gritMapping, skipUserConfirmation, includeS2S }) {
54
54
  const promises = [];
55
55
 
56
56
  const writeProviderPromise = await initProviderFile(targetRegion, outputDir);
@@ -206,8 +206,9 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
206
206
 
207
207
  if (isCompact || resourceName === 'ibm_cd_toolchain') {
208
208
  if (targetTag) newTfFileObj['resource']['ibm_cd_toolchain'][newTcId]['tags'] = [
209
- ...newTfFileObj['resource']['ibm_cd_toolchain'][newTcId]['tags'] ?? [],
210
- targetTag
209
+ Array.from(new Set( // uniqueness
210
+ (newTfFileObj['resource']['ibm_cd_toolchain'][newTcId]['tags'] ?? []).concat([targetTag])
211
+ ))
211
212
  ];
212
213
  if (targetToolchainName) newTfFileObj['resource']['ibm_cd_toolchain'][newTcId]['name'] = targetToolchainName;
213
214
  if (targetRgId) newTfFileObj['resource']['ibm_cd_toolchain'][newTcId]['resource_group_id'] = targetRgId;
@@ -339,7 +340,8 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
339
340
  }
340
341
 
341
342
  const newTfFileObjStr = JSON.stringify(newTfFileObj);
342
- const newTfFile = replaceDependsOn(jsonToTf(newTfFileObjStr));
343
+ let newTfFile = replaceDependsOn(jsonToTf(newTfFileObjStr));
344
+ if (includeS2S && (isCompact || resourceName === 'ibm_cd_toolchain')) newTfFile = addS2sScriptToToolchainTf(newTfFile);
343
345
  const copyResourcesPromise = writeFilePromise(`${outputDir}/${fileName}`, newTfFile);
344
346
  promises.push(copyResourcesPromise);
345
347
  }
@@ -382,11 +384,14 @@ async function getNumResourcesPlanned(dir) {
382
384
  };
383
385
  }
384
386
 
385
- async function runTerraformApply(skipTfConfirmation, outputDir, verbosity) {
387
+ async function runTerraformApply(skipTfConfirmation, outputDir, verbosity, target) {
386
388
  let command = 'terraform apply';
387
389
  if (skipTfConfirmation || verbosity === 0) {
388
390
  command = 'terraform apply -auto-approve';
389
391
  }
392
+ if (target) {
393
+ command += ` -target="${target}"`
394
+ }
390
395
 
391
396
  const child = child_process.spawn(command, {
392
397
  cwd: `${outputDir}`,
@@ -466,6 +471,25 @@ function replaceDependsOn(str) {
466
471
  }
467
472
  }
468
473
 
474
+ function addS2sScriptToToolchainTf(str) {
475
+ const provisionerStr = (tfName) => `\n\n provisioner "local-exec" {
476
+ command = "node create-s2s-script.js"
477
+ environment = {
478
+ IBMCLOUD_API_KEY = var.ibmcloud_api_key
479
+ TARGET_TOOLCHAIN_ID = ibm_cd_toolchain.${tfName}.id
480
+ }\n }`
481
+ try {
482
+ if (typeof str === 'string') {
483
+ const pattern = /^resource "ibm_cd_toolchain" "([a-z0-9_-]*)" \{$\n((.|\n)*)\n^\}$/gm;
484
+
485
+ // get rid of the quotes
486
+ return str.replace(pattern, (match, s1, s2) => `resource "ibm_cd_toolchain" "${s1}" {\n${s2}${provisionerStr(s1)}\n}`);
487
+ }
488
+ } catch {
489
+ return str;
490
+ }
491
+ }
492
+
469
493
  export {
470
494
  setTerraformEnv,
471
495
  initProviderFile,
package/config.js CHANGED
@@ -167,9 +167,9 @@ const SECRET_KEYS_MAP = {
167
167
  'nexus': [
168
168
  { key: 'token', tfKey: 'token' },
169
169
  ],
170
- 'pagerduty': [
171
- { key: 'service_key', tfKey: 'service_key', required: true },
172
- ],
170
+ // 'pagerduty': [
171
+ // { key: 'service_key', tfKey: 'service_key', required: true },
172
+ // ],
173
173
  'private_worker': [
174
174
  { key: 'workerQueueCredentials', tfKey: 'worker_queue_credentials', required: true },
175
175
  ],
@@ -205,7 +205,7 @@ const SUPPORTED_TOOLS_MAP = {
205
205
  'keyprotect': 'ibm_cd_toolchain_tool_keyprotect',
206
206
  'nexus': 'ibm_cd_toolchain_tool_nexus',
207
207
  'customtool': 'ibm_cd_toolchain_tool_custom',
208
- 'pagerduty': 'ibm_cd_toolchain_tool_pagerduty',
208
+ // 'pagerduty': 'ibm_cd_toolchain_tool_pagerduty',
209
209
  'saucelabs': 'ibm_cd_toolchain_tool_saucelabs',
210
210
  'secretsmanager': 'ibm_cd_toolchain_tool_secretsmanager',
211
211
  'security_compliance': 'ibm_cd_toolchain_tool_securitycompliance',
@@ -0,0 +1,119 @@
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 { resolve } from 'node:path';
12
+
13
+ const API_KEY = process.env['IBMCLOUD_API_KEY'];
14
+ if (!API_KEY) throw Error(`Missing 'IBMCLOUD_API_KEY'`);
15
+
16
+ const TC_ID = process.env['TARGET_TOOLCHAIN_ID'];
17
+ if (!TC_ID) throw Error(`Missing 'TARGET_TOOLCHAIN_ID'`);
18
+
19
+ 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
+
23
+ async function getBearer() {
24
+ const url = `${IAM_BASE_URL}/identity/token`;
25
+
26
+ const params = new URLSearchParams();
27
+ params.append('grant_type', 'urn:ibm:params:oauth:grant-type:apikey');
28
+ params.append('apikey', API_KEY);
29
+ params.append('response_type', 'cloud_iam');
30
+
31
+ try {
32
+ const response = await fetch(url, {
33
+ method: "POST",
34
+ headers: {
35
+ 'Accept': 'application/json',
36
+ 'Content-Type': 'application/x-www-form-urlencoded'
37
+ },
38
+ body: params
39
+ });
40
+
41
+ if (!response.ok) {
42
+ throw new Error(`Response status: ${response.status}, ${response.statusText}`);
43
+ }
44
+
45
+ console.log(`GETTING BEARER TOKEN... ${response.status}, ${response.statusText}`);
46
+
47
+ return (await response.json()).access_token;
48
+ } catch (error) {
49
+ console.error(error.message);
50
+ }
51
+ }
52
+
53
+ /* expecting item as an object with the format of:
54
+ {
55
+ "parameters": {
56
+ "name": "",
57
+ "integration-status": "",
58
+ "instance-id-type": "",
59
+ "region": "",
60
+ "resource-group": "",
61
+ "instance-name": "",
62
+ "instance-crn": "",
63
+ "setup-authorization-type": ""
64
+ },
65
+ "toolchainId": "",
66
+ "serviceId": "",
67
+ "env_id": ""
68
+ }
69
+ */
70
+
71
+ async function createS2sAuthPolicy(item) {
72
+ const url = `${CLOUD_PLATFORM}/devops/setup/api/v2/s2s_authorization?${new URLSearchParams({
73
+ toolchainId: TC_ID,
74
+ serviceId: item['serviceId'],
75
+ env_id: item['env_id']
76
+ }).toString()}`;
77
+
78
+ const data = JSON.stringify({
79
+ 'parameters': {
80
+ 'name': item['parameters']['name'],
81
+ 'integration-status': '',
82
+ 'instance-id-type': item['parameters']['instance-id-type'],
83
+ 'region': item['parameters']['region'],
84
+ 'resource-group': item['parameters']['resource-group'],
85
+ 'instance-name': item['parameters']['instance-name'],
86
+ 'instance-crn': item['parameters']['instance-crn'],
87
+ 'setup-authorization-type': 'select'
88
+ }
89
+ });
90
+
91
+ try {
92
+ const response = await fetch(url, {
93
+ method: "POST",
94
+ headers: {
95
+ 'Authorization': `Bearer ${bearer}`,
96
+ 'Content-Type': 'application/json',
97
+ },
98
+ body: data,
99
+ });
100
+
101
+ if (!response.ok) {
102
+ throw new Error(`Response status: ${response.status}, ${response.statusText}`);
103
+ }
104
+
105
+ console.log(`CREATING AUTH POLICY... ${response.status}, ${response.statusText}`);
106
+ } catch (error) {
107
+ console.error(error.message);
108
+ }
109
+ }
110
+
111
+ // main
112
+
113
+ const bearer = await getBearer();
114
+
115
+ const inputArr = JSON.parse(fs.readFileSync(resolve(INPUT_PATH)));
116
+
117
+ inputArr.forEach(async (item) => {
118
+ await createS2sAuthPolicy(item);
119
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibm-cloud/cd-tools",
3
- "version": "1.3.4",
3
+ "version": "1.4.0",
4
4
  "description": "Tools and utilities for the IBM Cloud Continuous Delivery service and resources",
5
5
  "repository": {
6
6
  "type": "git",