@electrolux-oss/plugin-infrawallet-backend 0.1.3 → 0.1.4

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/config.d.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  export interface Config {
2
2
  backend: {
3
3
  infraWallet: {
4
- /**
5
- * @deepVisibility secret
6
- */
7
4
  integrations: {
8
5
  azure?: [
9
6
  {
@@ -11,6 +8,9 @@ export interface Config {
11
8
  subscriptionId: string;
12
9
  clientId: string;
13
10
  tenantId: string;
11
+ /**
12
+ * @visibility secret
13
+ */
14
14
  clientSecret: string;
15
15
  tags?: string[];
16
16
  },
@@ -20,7 +20,13 @@ export interface Config {
20
20
  name: string;
21
21
  accountId: string;
22
22
  assumedRoleName: string;
23
+ /**
24
+ * @visibility secret
25
+ */
23
26
  accessKeyId?: string;
27
+ /**
28
+ * @visibility secret
29
+ */
24
30
  accessKeySecret?: string;
25
31
  tags?: string[];
26
32
  },
package/dist/index.cjs.js CHANGED
@@ -11,8 +11,8 @@ var clientSts = require('@aws-sdk/client-sts');
11
11
  var lodash = require('lodash');
12
12
  var moment = require('moment');
13
13
  var armCostmanagement = require('@azure/arm-costmanagement');
14
- var identity = require('@azure/identity');
15
14
  var coreRestPipeline = require('@azure/core-rest-pipeline');
15
+ var identity = require('@azure/identity');
16
16
  var bigquery = require('@google-cloud/bigquery');
17
17
 
18
18
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
@@ -24,34 +24,76 @@ var moment__default = /*#__PURE__*/_interopDefaultCompat(moment);
24
24
  async function getCategoryMappings(database, provider) {
25
25
  const result = {};
26
26
  const client = await database.getClient();
27
- const mappings = await client.where({ provider }).select().from("category_mappings");
28
- mappings.forEach((mapping) => {
27
+ const default_mappings = await client.where({ provider: provider.toLowerCase() }).select().from("category_mappings_default");
28
+ default_mappings.forEach((mapping) => {
29
+ if (typeof mapping.cloud_service_names === "string") {
30
+ JSON.parse(mapping.cloud_service_names).forEach((service) => {
31
+ result[service] = mapping.category;
32
+ });
33
+ } else {
34
+ mapping.cloud_service_names.forEach((service) => {
35
+ result[service] = mapping.category;
36
+ });
37
+ }
38
+ });
39
+ const override_mappings = await client.where({ provider }).select().from("category_mappings_override");
40
+ override_mappings.forEach((mapping) => {
29
41
  if (typeof mapping.cloud_service_names === "string") {
30
- result[mapping.category] = JSON.parse(mapping.cloud_service_names);
42
+ JSON.parse(mapping.cloud_service_names).forEach((service) => {
43
+ result[service] = mapping.category;
44
+ });
31
45
  } else {
32
- result[mapping.category] = mapping.cloud_service_names;
46
+ mapping.cloud_service_names.forEach((service) => {
47
+ result[service] = mapping.category;
48
+ });
33
49
  }
34
50
  });
35
51
  return result;
36
52
  }
37
53
  function getCategoryByServiceName(serviceName, categoryMappings) {
38
- for (const key of Object.keys(categoryMappings)) {
39
- const serviceNames = categoryMappings[key];
40
- if (serviceNames && serviceNames.includes(serviceName)) {
41
- return key;
42
- }
54
+ if (serviceName in categoryMappings) {
55
+ return categoryMappings[serviceName];
43
56
  }
44
57
  return "Uncategorized";
45
58
  }
59
+ async function getReportsFromCache(cache, provider, configKey, query) {
60
+ const cacheKey = [
61
+ provider,
62
+ configKey,
63
+ query.filters,
64
+ query.groups,
65
+ query.granularity,
66
+ query.startTime,
67
+ query.endTime
68
+ ].join("_");
69
+ const cachedCosts = await cache.get(cacheKey);
70
+ return cachedCosts;
71
+ }
72
+ async function setReportsToCache(cache, reports, provider, configKey, query, ttl) {
73
+ const cacheKey = [
74
+ provider,
75
+ configKey,
76
+ query.filters,
77
+ query.groups,
78
+ query.granularity,
79
+ query.startTime,
80
+ query.endTime
81
+ ].join("_");
82
+ await cache.set(cacheKey, reports, {
83
+ ttl: ttl
84
+ });
85
+ }
46
86
 
47
87
  class AwsClient {
48
- constructor(config, database, logger) {
88
+ constructor(providerName, config, database, cache, logger) {
89
+ this.providerName = providerName;
49
90
  this.config = config;
50
91
  this.database = database;
92
+ this.cache = cache;
51
93
  this.logger = logger;
52
94
  }
53
- static create(config, database, logger) {
54
- return new AwsClient(config, database, logger);
95
+ static create(config, database, cache, logger) {
96
+ return new AwsClient("AWS", config, database, cache, logger);
55
97
  }
56
98
  convertServiceName(serviceName) {
57
99
  let convertedName = serviceName;
@@ -61,16 +103,13 @@ class AwsClient {
61
103
  ["Virtual Private Cloud", "VPC (Virtual Private Cloud)"],
62
104
  ["Relational Database Service", "RDS (Relational Database Service)"],
63
105
  ["Simple Storage Service", "S3 (Simple Storage Service)"],
64
- [
65
- "Managed Streaming for Apache Kafka",
66
- "MSK (Managed Streaming for Apache Kafka)"
67
- ],
68
- [
69
- "Elastic Container Service for Kubernetes",
70
- "EKS (Elastic Container Service for Kubernetes)"
71
- ],
106
+ ["Managed Streaming for Apache Kafka", "MSK (Managed Streaming for Apache Kafka)"],
107
+ ["Elastic Container Service for Kubernetes", "EKS (Elastic Container Service for Kubernetes)"],
108
+ ["Elastic Container Service", "ECS (Elastic Container Service)"],
109
+ ["EC2 Container Registry (ECR)", "ECR (Elastic Container Registry)"],
72
110
  ["Simple Queue Service", "SQS (Simple Queue Service)"],
73
- ["Simple Notification Service", "SNS (Simple Notification Service)"]
111
+ ["Simple Notification Service", "SNS (Simple Notification Service)"],
112
+ ["Database Migration Service", "DMS (Database Migration Service)"]
74
113
  ]);
75
114
  for (const prefix of prefixes) {
76
115
  if (serviceName.startsWith(prefix)) {
@@ -80,24 +119,31 @@ class AwsClient {
80
119
  if (aliases.has(convertedName)) {
81
120
  convertedName = aliases.get(convertedName) || convertedName;
82
121
  }
83
- return `AWS/${convertedName}`;
122
+ return `${this.providerName}/${convertedName}`;
84
123
  }
85
124
  async fetchCostsFromCloud(query) {
86
- const conf = this.config.getOptionalConfigArray(
87
- "backend.infraWallet.integrations.aws"
88
- );
125
+ const conf = this.config.getOptionalConfigArray("backend.infraWallet.integrations.aws");
89
126
  if (!conf) {
90
- return [];
127
+ return { reports: [], errors: [] };
91
128
  }
92
129
  const promises = [];
93
130
  const results = [];
131
+ const errors = [];
94
132
  query.groups.split(",").forEach((group) => {
95
133
  if (group.includes(":")) {
96
134
  group.split(":");
97
135
  }
98
136
  });
99
137
  for (const c of conf) {
100
- const name = c.getString("name");
138
+ const accountName = c.getString("name");
139
+ const cachedCosts = await getReportsFromCache(this.cache, this.providerName, accountName, query);
140
+ if (cachedCosts) {
141
+ this.logger.debug(`${this.providerName}/${accountName} costs from cache`);
142
+ cachedCosts.map((cost) => {
143
+ results.push(cost);
144
+ });
145
+ continue;
146
+ }
101
147
  const accountId = c.getString("accountId");
102
148
  const assumedRoleName = c.getString("assumedRoleName");
103
149
  const accessKeyId = c.getOptionalString("accessKeyId");
@@ -108,7 +154,7 @@ class AwsClient {
108
154
  const [k, v] = tag.split(":");
109
155
  tagKeyValues[k.trim()] = v.trim();
110
156
  });
111
- const categoryMappings = await getCategoryMappings(this.database, "aws");
157
+ const categoryMappings = await getCategoryMappings(this.database, this.providerName);
112
158
  let stsParams = {};
113
159
  if (accessKeyId && accessKeySecret) {
114
160
  stsParams = {
@@ -178,26 +224,21 @@ class AwsClient {
178
224
  row.Groups.forEach((group) => {
179
225
  var _a3;
180
226
  const serviceName = group.Keys ? group.Keys[0] : "";
181
- const keyName = `${name}_${serviceName}`;
227
+ const keyName = `${accountName}_${serviceName}`;
182
228
  if (!accumulator[keyName]) {
183
229
  accumulator[keyName] = {
184
230
  id: keyName,
185
- name: `AWS/${name}`,
231
+ name: `${this.providerName}/${accountName}`,
186
232
  service: this.convertServiceName(serviceName),
187
- category: getCategoryByServiceName(
188
- serviceName,
189
- categoryMappings
190
- ),
191
- provider: "AWS",
233
+ category: getCategoryByServiceName(serviceName, categoryMappings),
234
+ provider: this.providerName,
192
235
  reports: {},
193
236
  ...tagKeyValues
194
237
  };
195
238
  }
196
239
  const groupMetrics = group.Metrics;
197
240
  if (groupMetrics !== void 0) {
198
- accumulator[keyName].reports[period] = parseFloat(
199
- (_a3 = groupMetrics.UnblendedCost.Amount) != null ? _a3 : "0.0"
200
- );
241
+ accumulator[keyName].reports[period] = parseFloat((_a3 = groupMetrics.UnblendedCost.Amount) != null ? _a3 : "0.0");
201
242
  }
202
243
  });
203
244
  }
@@ -205,28 +246,46 @@ class AwsClient {
205
246
  },
206
247
  {}
207
248
  );
249
+ await setReportsToCache(
250
+ this.cache,
251
+ Object.values(transformedData),
252
+ this.providerName,
253
+ accountName,
254
+ query,
255
+ 60 * 60 * 2 * 1e3
256
+ );
208
257
  Object.values(transformedData).map((value) => {
209
258
  results.push(value);
210
259
  });
211
260
  } catch (e) {
212
261
  this.logger.error(e);
262
+ errors.push({
263
+ provider: this.providerName,
264
+ name: `${this.providerName}/${accountName}`,
265
+ error: e.message
266
+ });
213
267
  }
214
268
  })();
215
269
  promises.push(promise);
216
270
  }
217
271
  await Promise.all(promises);
218
- return results;
272
+ return {
273
+ reports: results,
274
+ errors
275
+ };
219
276
  }
220
277
  }
221
278
 
222
279
  class AzureClient {
223
- constructor(config, database, logger) {
280
+ constructor(providerName, config, database, cache, logger) {
281
+ this.providerName = providerName;
224
282
  this.config = config;
225
283
  this.database = database;
284
+ this.cache = cache;
226
285
  this.logger = logger;
227
286
  }
228
- static create(config, database, logger) {
229
- return new AzureClient(config, database, logger);
287
+ static create(config, database, cache, logger) {
288
+ return new AzureClient("Azure", config, database, cache, logger);
230
289
  }
231
290
  convertServiceName(serviceName) {
232
291
  let convertedName = serviceName;
@@ -236,7 +295,7 @@ class AzureClient {
236
295
  convertedName = serviceName.slice(prefix.length).trim();
237
296
  }
238
297
  }
239
- return `Azure/${convertedName}`;
298
+ return `${this.providerName}/${convertedName}`;
240
299
  }
241
300
  formatDate(dateNumber) {
242
301
  const dateString = dateNumber.toString();
@@ -263,7 +322,10 @@ class AzureClient {
263
322
  if (response.status === 200) {
264
323
  return JSON.parse(response.bodyAsText || "{}");
265
324
  } else if (response.status === 429) {
266
- const retryAfter = parseInt(response.headers.get("x-ms-ratelimit-microsoft.costmanagement-entity-retry-after") || "60", 10);
325
+ const retryAfter = parseInt(
326
+ response.headers.get("x-ms-ratelimit-microsoft.costmanagement-entity-retry-after") || "60",
327
+ 10
328
+ );
267
329
  this.logger.warn(`Hit Azure rate limit, retrying after ${retryAfter} seconds...`);
268
330
  await new Promise((resolve) => setTimeout(resolve, retryAfter * 1e3));
269
331
  retries++;
@@ -297,30 +359,30 @@ class AzureClient {
297
359
  return allResults;
298
360
  }
299
361
  async fetchCostsFromCloud(query) {
300
- const conf = this.config.getOptionalConfigArray(
301
- "backend.infraWallet.integrations.azure"
302
- );
362
+ const conf = this.config.getOptionalConfigArray("backend.infraWallet.integrations.azure");
303
363
  if (!conf) {
304
- return [];
364
+ return { reports: [], errors: [] };
305
365
  }
306
- const categoryMappings = await getCategoryMappings(
307
- this.database,
308
- "azure"
309
- );
366
+ const categoryMappings = await getCategoryMappings(this.database, this.providerName);
310
367
  const promises = [];
311
368
  const results = [];
369
+ const errors = [];
312
370
  const groupPairs = [{ type: "Dimension", name: "ServiceName" }];
313
371
  for (const c of conf) {
314
- const name = c.getString("name");
372
+ const accountName = c.getString("name");
373
+ const cachedCosts = await getReportsFromCache(this.cache, this.providerName, accountName, query);
374
+ if (cachedCosts) {
375
+ this.logger.debug(`${this.providerName}/${accountName} costs from cache`);
376
+ cachedCosts.map((cost) => {
377
+ results.push(cost);
378
+ });
379
+ continue;
380
+ }
315
381
  const subscriptionId = c.getString("subscriptionId");
316
382
  const tenantId = c.getString("tenantId");
317
383
  const clientId = c.getString("clientId");
318
384
  const clientSecret = c.getString("clientSecret");
319
- const credential = new identity.ClientSecretCredential(
320
- tenantId,
321
- clientId,
322
- clientSecret
323
- );
385
+ const credential = new identity.ClientSecretCredential(tenantId, clientId, clientSecret);
324
386
  const client = new armCostmanagement.CostManagementClient(credential);
325
387
  const tags = c.getOptionalStringArray("tags");
326
388
  const tagKeyValues = {};
@@ -347,17 +409,17 @@ class AzureClient {
347
409
  if (query.granularity.toUpperCase() === "DAILY") {
348
410
  date = this.formatDate(date);
349
411
  }
350
- let keyName = name;
412
+ let keyName = accountName;
351
413
  for (let i = 0; i < groupPairs.length; i++) {
352
414
  keyName += `->${row[i + 2]}`;
353
415
  }
354
416
  if (!accumulator[keyName]) {
355
417
  accumulator[keyName] = {
356
418
  id: keyName,
357
- name: `Azure/${name}`,
419
+ name: `${this.providerName}/${accountName}`,
358
420
  service: this.convertServiceName(serviceName),
359
421
  category: getCategoryByServiceName(serviceName, categoryMappings),
360
- provider: "Azure",
422
+ provider: this.providerName,
361
423
  reports: {},
362
424
  ...tagKeyValues
363
425
  };
@@ -374,38 +436,56 @@ class AzureClient {
374
436
  },
375
437
  {}
376
438
  );
439
+ await setReportsToCache(
440
+ this.cache,
441
+ Object.values(transformedData),
442
+ this.providerName,
443
+ accountName,
444
+ query,
445
+ 60 * 60 * 2 * 1e3
446
+ );
377
447
  Object.values(transformedData).map((value) => {
378
448
  results.push(value);
379
449
  });
380
450
  } catch (e) {
381
451
  this.logger.error(e);
452
+ errors.push({
453
+ provider: this.providerName,
454
+ name: `${this.providerName}/${accountName}`,
455
+ error: e.message
456
+ });
382
457
  }
383
458
  })();
384
459
  promises.push(promise);
385
460
  }
386
461
  await Promise.all(promises);
387
- return results;
462
+ return {
463
+ reports: results,
464
+ errors
465
+ };
388
466
  }
389
467
  }
390
468
 
391
469
  class GCPClient {
392
- constructor(config, database, logger) {
470
+ constructor(providerName, config, database, cache, logger) {
471
+ this.providerName = providerName;
393
472
  this.config = config;
394
473
  this.database = database;
474
+ this.cache = cache;
395
475
  this.logger = logger;
396
476
  }
397
- static create(config, database, logger) {
398
- return new GCPClient(config, database, logger);
477
+ static create(config, database, cache, logger) {
478
+ return new GCPClient("GCP", config, database, cache, logger);
399
479
  }
400
480
  convertServiceName(serviceName) {
401
481
  let convertedName = serviceName;
402
- const prefixes = ["GCP"];
482
+ const prefixes = ["Google Cloud"];
403
483
  for (const prefix of prefixes) {
404
484
  if (serviceName.startsWith(prefix)) {
405
485
  convertedName = serviceName.slice(prefix.length).trim();
406
486
  }
407
487
  }
408
- return `GCP/${convertedName}`;
488
+ return `${this.providerName}/${convertedName}`;
409
489
  }
410
490
  async queryBigQuery(keyFilePath, projectId, datasetId, tableId, query) {
411
491
  const options = {
@@ -443,16 +523,23 @@ class GCPClient {
443
523
  }
444
524
  }
445
525
  async fetchCostsFromCloud(query) {
446
- const conf = this.config.getOptionalConfigArray(
447
- "backend.infraWallet.integrations.gcp"
448
- );
526
+ const conf = this.config.getOptionalConfigArray("backend.infraWallet.integrations.gcp");
449
527
  if (!conf) {
450
- return [];
528
+ return { reports: [], errors: [] };
451
529
  }
452
530
  const promises = [];
453
531
  const results = [];
532
+ const errors = [];
454
533
  for (const c of conf) {
455
- const name = c.getString("name");
534
+ const accountName = c.getString("name");
535
+ const cachedCosts = await getReportsFromCache(this.cache, this.providerName, accountName, query);
536
+ if (cachedCosts) {
537
+ this.logger.debug(`${this.providerName}/${accountName} costs from cache`);
538
+ cachedCosts.map((cost) => {
539
+ results.push(cost);
540
+ });
541
+ continue;
542
+ }
456
543
  const keyFilePath = c.getString("keyFilePath");
457
544
  const projectId = c.getString("projectId");
458
545
  const datasetId = c.getString("datasetId");
@@ -463,31 +550,22 @@ class GCPClient {
463
550
  const [k, v] = tag.split(":");
464
551
  tagKeyValues[k.trim()] = v.trim();
465
552
  });
466
- const categoryMappings = await getCategoryMappings(this.database, "gcp");
553
+ const categoryMappings = await getCategoryMappings(this.database, this.providerName);
467
554
  const promise = (async () => {
468
555
  try {
469
- const costResponse = await this.queryBigQuery(
470
- keyFilePath,
471
- projectId,
472
- datasetId,
473
- tableId,
474
- query
475
- );
556
+ const costResponse = await this.queryBigQuery(keyFilePath, projectId, datasetId, tableId, query);
476
557
  const transformedData = lodash.reduce(
477
558
  costResponse,
478
559
  (acc, row) => {
479
560
  const period = row.period;
480
- const keyName = `${name}_${row.project}_${row.service}`;
561
+ const keyName = `${accountName}_${row.project}_${row.service}`;
481
562
  if (!acc[keyName]) {
482
563
  acc[keyName] = {
483
564
  id: keyName,
484
- name: `GCP/${name}`,
565
+ name: `${this.providerName}/${accountName}`,
485
566
  service: this.convertServiceName(row.service),
486
- category: getCategoryByServiceName(
487
- row.service,
488
- categoryMappings
489
- ),
490
- provider: "GCP",
567
+ category: getCategoryByServiceName(row.service, categoryMappings),
568
+ provider: this.providerName,
491
569
  reports: {},
492
570
  ...{ project: row.project },
493
571
  // TODO: how should we handle the project field? for now, we add project name as a field in the report
@@ -500,51 +578,56 @@ class GCPClient {
500
578
  },
501
579
  {}
502
580
  );
581
+ await setReportsToCache(
582
+ this.cache,
583
+ Object.values(transformedData),
584
+ this.providerName,
585
+ accountName,
586
+ query,
587
+ 60 * 60 * 2 * 1e3
588
+ );
503
589
  Object.values(transformedData).map((value) => {
504
590
  results.push(value);
505
591
  });
506
592
  } catch (e) {
507
593
  this.logger.error(e);
594
+ errors.push({
595
+ provider: this.providerName,
596
+ name: `${this.providerName}/${accountName}`,
597
+ error: e.message
598
+ });
508
599
  }
509
600
  })();
510
601
  promises.push(promise);
511
602
  }
512
603
  await Promise.all(promises);
513
- return results;
604
+ return {
605
+ reports: results,
606
+ errors
607
+ };
514
608
  }
515
609
  }
516
610
 
517
611
  async function setUpDatabase(database) {
518
612
  var _a;
519
613
  const client = await database.getClient();
520
- const migrationsDir = backendPluginApi.resolvePackagePath(
521
- "@electrolux-oss/plugin-infrawallet-backend",
522
- "migrations"
523
- );
614
+ const migrationsDir = backendPluginApi.resolvePackagePath("@electrolux-oss/plugin-infrawallet-backend", "migrations");
524
615
  if (!((_a = database.migrations) == null ? void 0 : _a.skip)) {
525
616
  await client.migrate.latest({
526
617
  directory: migrationsDir
527
618
  });
528
619
  }
529
- const category_mappings_count = await client("category_mappings").count(
530
- "id as c"
531
- );
532
- if (category_mappings_count[0].c === 0 || category_mappings_count[0].c === "0") {
533
- const seedsDir = backendPluginApi.resolvePackagePath(
534
- "@electrolux-oss/plugin-infrawallet-backend",
535
- "seeds"
536
- );
537
- await client.seed.run({ directory: seedsDir });
538
- }
620
+ const seedsDir = backendPluginApi.resolvePackagePath("@electrolux-oss/plugin-infrawallet-backend", "seeds");
621
+ await client.seed.run({ directory: seedsDir });
539
622
  }
540
623
  async function createRouter(options) {
541
624
  const { logger, config, cache, database } = options;
542
625
  await setUpDatabase(database);
543
626
  const router = Router__default.default();
544
627
  router.use(express__default.default.json());
545
- const azureClient = AzureClient.create(config, database, logger);
546
- const awsClient = AwsClient.create(config, database, logger);
547
- const gcpClient = GCPClient.create(config, database, logger);
628
+ const azureClient = AzureClient.create(config, database, cache, logger);
629
+ const awsClient = AwsClient.create(config, database, cache, logger);
630
+ const gcpClient = GCPClient.create(config, database, cache, logger);
548
631
  const cloudClients = [azureClient, awsClient, gcpClient];
549
632
  router.get("/health", (_, response) => {
550
633
  logger.info("PONG!");
@@ -558,46 +641,40 @@ async function createRouter(options) {
558
641
  const endTime = request.query.endTime;
559
642
  const promises = [];
560
643
  const results = [];
644
+ const errors = [];
561
645
  cloudClients.forEach(async (client) => {
562
646
  const fetchCloudCosts = (async () => {
563
- const cacheKey = [
564
- client.constructor.name,
565
- filters,
566
- groups,
567
- granularity,
568
- startTime,
569
- endTime
570
- ].join("_");
571
- const cachedCosts = await cache.get(cacheKey);
572
- if (cachedCosts) {
573
- logger.debug(`${client.constructor.name} costs from cache`);
574
- cachedCosts.forEach((cost) => {
647
+ try {
648
+ const clientResponse = await client.fetchCostsFromCloud({
649
+ filters,
650
+ groups,
651
+ granularity,
652
+ startTime,
653
+ endTime
654
+ });
655
+ clientResponse.errors.forEach((e) => {
656
+ errors.push(e);
657
+ });
658
+ clientResponse.reports.forEach((cost) => {
575
659
  results.push(cost);
576
660
  });
577
- } else {
578
- try {
579
- const costs = await client.fetchCostsFromCloud({
580
- filters,
581
- groups,
582
- granularity,
583
- startTime,
584
- endTime
585
- });
586
- await cache.set(cacheKey, costs, {
587
- ttl: 60 * 60 * 2 * 1e3
588
- });
589
- costs.forEach((cost) => {
590
- results.push(cost);
591
- });
592
- } catch (e) {
593
- logger.error(e);
594
- }
661
+ } catch (e) {
662
+ logger.error(e);
663
+ errors.push({
664
+ provider: client.constructor.name,
665
+ name: client.constructor.name,
666
+ error: e.message
667
+ });
595
668
  }
596
669
  })();
597
670
  promises.push(fetchCloudCosts);
598
671
  });
599
672
  await Promise.all(promises);
600
- response.json({ data: results, status: "ok" });
673
+ if (errors.length > 0) {
674
+ response.status(207).json({ data: results, errors, status: 207 });
675
+ } else {
676
+ response.json({ data: results, errors, status: 200 });
677
+ }
601
678
  });
602
679
  router.use(backendCommon.errorHandler());
603
680
  return router;