@ibm-cloud/cd-tools 1.0.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.
@@ -0,0 +1,359 @@
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 axios from 'axios';
11
+ import axiosRetry from 'axios-retry';
12
+
13
+ import { logger, LOG_STAGES } from './logger.js';
14
+
15
+ axiosRetry(axios, {
16
+ retries: 3,
17
+ retryDelay: axiosRetry.exponentialDelay,
18
+ retryCondition: (error) => {
19
+ return axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response?.status === 500;
20
+ },
21
+ });
22
+
23
+ axios.interceptors.request.use(request => {
24
+ logger.debug(`${request.method.toUpperCase()} ${request.url}`, LOG_STAGES.setup);
25
+ if (request.data) {
26
+ const body = typeof request.data === 'string'
27
+ ? request.data
28
+ : JSON.stringify(request.data);
29
+ logger.log(`Https Request body: ${body}`, LOG_STAGES.setup);
30
+ }
31
+ return request;
32
+ });
33
+
34
+ axios.interceptors.response.use(response => {
35
+ if (response.data) {
36
+ let body = typeof response.data === 'string'
37
+ ? response.data
38
+ : JSON.stringify(response.data);
39
+ if (response.data.access_token) // Redact user access token in logs
40
+ body = body.replaceAll(response.data.access_token, '<USER ACCESS TOKEN>');
41
+ logger.log(`Https Response body: ${body}`, LOG_STAGES.setup);
42
+ }
43
+ return response;
44
+ }, error => {
45
+ if (error.response) {
46
+ logger.log(`Error response status: ${error.response.status} ${error.response.statusText}`);
47
+ logger.log(`Error response body: ${JSON.stringify(error.response.data)}`);
48
+ } else {
49
+ logger.log(`Error message: ${error.message}`);
50
+ }
51
+ return Promise.reject(error);
52
+ });
53
+
54
+ async function getBearerToken(apiKey) {
55
+ const iamUrl = 'https://iam.cloud.ibm.com/identity/token';
56
+ const params = new URLSearchParams();
57
+ params.append('grant_type', 'urn:ibm:params:oauth:grant-type:apikey');
58
+ params.append('apikey', apiKey);
59
+ params.append('response_type', 'cloud_iam');
60
+ const options = {
61
+ method: 'POST',
62
+ url: iamUrl,
63
+ headers: {
64
+ 'Accept': 'application/json',
65
+ 'Content-Type': 'application/x-www-form-urlencoded'
66
+ },
67
+ data: params,
68
+ validateStatus: () => true
69
+ };
70
+ const response = await axios(options);
71
+ if (response.status !== 200) {
72
+ throw Error('There was a problem getting a bearer token using IBMCLOUD_API_KEY');
73
+ }
74
+ return response.data.access_token;
75
+ }
76
+
77
+ async function getAccountId(bearer, apiKey) {
78
+ const iamUrl = 'https://iam.cloud.ibm.com/v1/apikeys/details';
79
+ const options = {
80
+ method: 'GET',
81
+ url: iamUrl,
82
+ headers: {
83
+ 'Authorization': `Bearer ${bearer}`,
84
+ 'Content-Type': 'application/json',
85
+ 'IAM-ApiKey': apiKey
86
+ },
87
+ validateStatus: () => true
88
+ };
89
+ const response = await axios(options);
90
+ if (response.status !== 200) {
91
+ throw Error('There was a problem getting account_id using IBMCLOUD_API_KEY');
92
+ }
93
+ return response.data.account_id;
94
+ }
95
+
96
+ async function getToolchain(bearer, toolchainId, region) {
97
+ const apiBaseUrl = `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
98
+ const options = {
99
+ method: 'GET',
100
+ url: `${apiBaseUrl}/toolchains/${toolchainId}`,
101
+ headers: {
102
+ 'Accept': 'application/json',
103
+ 'Authorization': `Bearer ${bearer}`,
104
+ 'Content-Type': 'application/json',
105
+ },
106
+ validateStatus: () => true
107
+ };
108
+ const response = await axios(options);
109
+ switch (response.status) {
110
+ case 200:
111
+ return response.data;
112
+ case 404:
113
+ throw Error('The toolchain with provided CRN was not found or is not accessible');
114
+ default:
115
+ throw Error(response.statusText);
116
+ }
117
+ }
118
+
119
+ async function getToolchainsByName(bearer, accountId, toolchainName) {
120
+ const apiBaseUrl = 'https://api.global-search-tagging.cloud.ibm.com/v3';
121
+ const options = {
122
+ url: apiBaseUrl + '/resources/search',
123
+ method: 'POST',
124
+ headers: {
125
+ 'Authorization': `Bearer ${bearer}`,
126
+ 'Content-Type': 'application/json',
127
+ },
128
+ data: {
129
+ 'query': `service_name:toolchain AND name:"${toolchainName}" AND doc.state:ACTIVE`,
130
+ 'fields': ['doc.resource_group_id', 'doc.region_id']
131
+ },
132
+ params: { account_id: accountId },
133
+ validateStatus: () => true
134
+ };
135
+ const response = await axios(options);
136
+ switch (response.status) {
137
+ case 200:
138
+ return response.data.items.map(item => { return { resource_group_id: item.doc.resource_group_id, region_id: item.doc.region_id } });
139
+ default:
140
+ throw Error('Get toolchains failed');
141
+ }
142
+ }
143
+
144
+ async function getToolchainTools(bearer, toolchainId, region) {
145
+ const apiBaseUrl = `https://api.${region}.devops.cloud.ibm.com/toolchain/v2`;
146
+ const options = {
147
+ method: 'GET',
148
+ url: `${apiBaseUrl}/toolchains/${toolchainId}/tools`,
149
+ headers: {
150
+ 'Accept': 'application/json',
151
+ 'Authorization': `Bearer ${bearer}`,
152
+ 'Content-Type': 'application/json',
153
+ },
154
+ params: { limit: 150 },
155
+ validateStatus: () => true
156
+ };
157
+ const response = await axios(options);
158
+ switch (response.status) {
159
+ case 200:
160
+ return response.data;
161
+ default:
162
+ throw Error(response.statusText);
163
+ }
164
+ }
165
+
166
+ async function getPipelineData(bearer, pipelineId, region) {
167
+ const apiBaseUrl = `https://api.${region}.devops.cloud.ibm.com/pipeline/v2`;
168
+ const options = {
169
+ method: 'GET',
170
+ url: `${apiBaseUrl}/tekton_pipelines/${pipelineId}`,
171
+ headers: {
172
+ 'Accept': 'application/json',
173
+ 'Authorization': `Bearer ${bearer}`,
174
+ 'Content-Type': 'application/json',
175
+ },
176
+ validateStatus: () => true
177
+ };
178
+ const response = await axios(options);
179
+ switch (response.status) {
180
+ case 200:
181
+ return response.data;
182
+ default:
183
+ throw Error(response.statusText);
184
+ }
185
+ }
186
+
187
+ // takes in resource group ID or name
188
+ async function getResourceGroupId(bearer, accountId, resourceGroup) {
189
+ const apiBaseUrl = 'https://api.global-search-tagging.cloud.ibm.com/v3';
190
+ const options = {
191
+ url: apiBaseUrl + '/resources/search',
192
+ method: 'POST',
193
+ headers: {
194
+ 'Authorization': `Bearer ${bearer}`,
195
+ 'Content-Type': 'application/json',
196
+ },
197
+ data: {
198
+ 'query': `type:resource-group AND (name:${resourceGroup} OR doc.id:${resourceGroup}) AND doc.state:ACTIVE`,
199
+ 'fields': ['doc.id']
200
+ },
201
+ params: { account_id: accountId },
202
+ validateStatus: () => true
203
+ };
204
+ const response = await axios(options);
205
+ switch (response.status) {
206
+ case 200:
207
+ if (response.data.items.length != 1) throw Error('The resource group with provided ID or name was not found or is not accessible');
208
+ return response.data.items[0].doc.id;
209
+ default:
210
+ throw Error('The resource group with provided ID or name was not found or is not accessible');
211
+ }
212
+ }
213
+
214
+ async function getAppConfigHealthcheck(bearer, tcId, toolId, region) {
215
+ const apiBaseUrl = 'https://cloud.ibm.com/devops/api/v1';
216
+ const options = {
217
+ url: apiBaseUrl + '/appconfig/healthcheck',
218
+ method: 'GET',
219
+ headers: {
220
+ 'Authorization': `Bearer ${bearer}`,
221
+ 'Content-Type': 'application/json',
222
+ },
223
+ params: { toolchainId: tcId, serviceId: toolId, env_id: `ibm:yp:${region}` },
224
+ validateStatus: () => true
225
+ };
226
+ const response = await axios(options);
227
+ switch (response.status) {
228
+ case 200:
229
+ return
230
+ default:
231
+ throw Error('Healthcheck failed');
232
+ }
233
+ }
234
+
235
+ async function getSecretsHealthcheck(bearer, tcId, toolName, region) {
236
+ const apiBaseUrl = 'https://cloud.ibm.com/devops/api/v1';
237
+ const options = {
238
+ url: apiBaseUrl + '/secrets/healthcheck',
239
+ method: 'GET',
240
+ headers: {
241
+ 'Authorization': `Bearer ${bearer}`,
242
+ 'Content-Type': 'application/json',
243
+ },
244
+ params: { toolchainId: tcId, integrationName: toolName, env_id: `ibm:yp:${region}` },
245
+ validateStatus: () => true
246
+ };
247
+ const response = await axios(options);
248
+ switch (response.status) {
249
+ case 200:
250
+ return
251
+ default:
252
+ throw Error('Healthcheck failed');
253
+ }
254
+ }
255
+
256
+ async function getGitOAuth(bearer, targetRegion, gitId) {
257
+ const url = 'https://cloud.ibm.com/devops/git/api/v1/tokens';
258
+ const options = {
259
+ url: url,
260
+ method: 'GET',
261
+ headers: {
262
+ 'Authorization': `Bearer ${bearer}`,
263
+ 'Content-Type': 'application/json',
264
+ },
265
+ // TODO: replace return_uri with "official" endpoint
266
+ params: { env_id: `ibm:yp:${targetRegion}`, git_id: gitId, console_url: 'https://cloud.ibm.com', return_uri: `https://cloud.ibm.com/devops/git?env_id=ibm:yp:${targetRegion}` },
267
+ validateStatus: () => true
268
+ };
269
+ const response = await axios(options);
270
+ switch (response.status) {
271
+ case 200:
272
+ return response.data?.access_token;
273
+ case 500:
274
+ throw Error(response.data?.authorizationURI);
275
+ default:
276
+ throw Error('Get git OAuth failed');
277
+ }
278
+ }
279
+
280
+ async function getGritUserProject(privToken, region, user, projectName) {
281
+ const url = `https://${region}.git.cloud.ibm.com/api/v4/users/${user}/projects`
282
+ const options = {
283
+ url: url,
284
+ method: 'GET',
285
+ headers: {
286
+ 'PRIVATE-TOKEN': privToken
287
+ },
288
+ params: { simple: true, search: projectName },
289
+ validateStatus: () => true
290
+ };
291
+ const response = await axios(options);
292
+ switch (response.status) {
293
+ case 200:
294
+ const found = response.data?.find((entry) => entry['path'] === projectName);
295
+ if (!found) throw Error('GRIT user project not found');
296
+ return;
297
+ default:
298
+ throw Error('Get GRIT user project failed');
299
+ }
300
+ }
301
+
302
+ async function getGritGroup(privToken, region, groupName) {
303
+ const url = `https://${region}.git.cloud.ibm.com/api/v4/groups`
304
+ const options = {
305
+ url: url,
306
+ method: 'GET',
307
+ headers: {
308
+ 'PRIVATE-TOKEN': privToken
309
+ },
310
+ params: { simple: true, search: groupName },
311
+ validateStatus: () => true
312
+ };
313
+ const response = await axios(options);
314
+ switch (response.status) {
315
+ case 200:
316
+ const found = response.data?.find((entry) => entry['full_path'] === groupName);
317
+ if (!found) throw Error('GRIT group not found');
318
+ return found['id'];
319
+ default:
320
+ throw Error('Get GRIT group failed');
321
+ }
322
+ }
323
+ async function getGritGroupProject(privToken, region, groupId, projectName) {
324
+ const url = `https://${region}.git.cloud.ibm.com/api/v4/groups/${groupId}/projects`
325
+ const options = {
326
+ url: url,
327
+ method: 'GET',
328
+ headers: {
329
+ 'PRIVATE-TOKEN': privToken
330
+ },
331
+ params: { simple: true, search: projectName },
332
+ validateStatus: () => true
333
+ };
334
+ const response = await axios(options);
335
+ switch (response.status) {
336
+ case 200:
337
+ const found = response.data?.find((entry) => entry['path'] === projectName);
338
+ if (!found) throw Error('GRIT group project not found');
339
+ return;
340
+ default:
341
+ throw Error('Get GRIT group project failed');
342
+ }
343
+ }
344
+
345
+ export {
346
+ getBearerToken,
347
+ getAccountId,
348
+ getToolchain,
349
+ getToolchainsByName,
350
+ getToolchainTools,
351
+ getPipelineData,
352
+ getResourceGroupId,
353
+ getAppConfigHealthcheck,
354
+ getSecretsHealthcheck,
355
+ getGitOAuth,
356
+ getGritUserProject,
357
+ getGritGroup,
358
+ getGritGroupProject
359
+ }